'MVVM in TableView Cell

hope you're well, i am working on an app which uses TableView for showing feeds to user using ViewModel and my ViewModel contains a variable which contains data of all cells and ViewModel also contains other data as well, what i am doing is passing whole ViewModel reference and indexPath to cell, here you can see:

func configureCell(feedsViewModelObj feedsViewModel: FeedsViewModel, cellIndexPath: IndexPath, presentingVC: UIViewController){
    //Assigning on global variables
    self.feedsViewModel = feedsViewModel
    self.cellIndexPath = cellIndexPath
    self.presentingVC = presentingVC
   
    let postData = feedsViewModel.feedsData!.data[cellIndexPath.row]
    
    //Populate
    nameLabel.text = postData.userDetails.name
    userImageView.sd_setImage(with: URL(string: postData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))
    
    updateTimeAgo()
    postTextLabel.text = postData.description
    upvoteBtn.setTitle(postData.totalBull.toString(), for: .normal)
    upvoteBtn.setSelected(selected: postData.isClickedBull, isAnimated: false)
    downvoteBtn.setSelected(selected: postData.isClickedBear, isAnimated: false)
    downvoteBtn.setTitle(postData.totalBear.toString(), for: .normal)
    commentbtn.setTitle(postData.totalComments.toString(), for: .normal)
    optionsBtn.isHidden = !(postData.canEdit && postData.canDelete)
    
    populateMedia(mediaData: postData.files)
}

so, is it the right or good way to pass full ViewModel reference and index to cell and then each cell access its data from data array? (if not please guide me). Many thanks.



Solution 1:[1]

*Passing whole ViewModel reference and indexPath to cell is not necessary. Call back after receiving data:

ViewController -> ViewModel -> TableViewDatasource -> TableViewCell.*

ViewController

 class ViewController: UIViewController {
        var viewModel: ViewModel?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            TaxiDetailsViewModelCall()
        }
        
        func TaxiDetailsViewModelCall() {
            viewModel = ViewModel()
            viewModel?.fetchFeedsData(completion: {
                self?.tableViewDatasource = TableViewDatasource(_feedsData:modelview?.feedsData ?? [FeedsData]())
                DispatchQueue.main.async {
                    self.tableView.dataSource = self.tableViewDatasource
                    self.tableView.reloadData()
                }
           })
        }
    }

View Model

class ViewModel {
var feedsData = [FeedsData]()
    func fetchFeedsData(completion: () -> ())  {
        let _manager = NetworkManager()
        _manager.networkRequest(_url: url, _modelType: FeedsData.self, _sucessData: { data in
            self.feedsData.accept(data)
       completion()
        })
    }
}

TableView Datasource

 class TableViewDatasource: NSObject,UITableViewDataSource {
        
        var feedsData: [FeedsData]?
        init(_feedsData: [FeedsData]) {
            feedsData = _feedsData
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return feedsData.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withReuseIdentifier: "TableViewCellName", for: indexPath) as? TableViewViewCell else {
                return TableViewViewCell()
            }
            cell.initialiseOutlet(_feedsData: feedsData[indexPath.row])
            return cell
        }
    }

TableView Cell

class TableViewCell: UITableViewCell {
            
            @IBOutlet weak var nameLabel : UILabel!
            @IBOutlet weak var userImageView : UIImageView!
            @IBOutlet weak var postTextLabel : UILabel!
            @IBOutlet weak var upvoteBtn : UIButton!
            @IBOutlet weak var downvoteBtn : UIButton!
            @IBOutlet weak var commentbtn : UIButton!
            @IBOutlet weak var optionsBtn : UIButton!
            
            
            override func awakeFromNib() {
                super.awakeFromNib()
            }
            
            /*
             Passing feedsData Object from TableViewDatasource
             */
            func initialiseOutlet(_feedsData: feedsData) {
                nameLabel.text = _feedsData.userDetails.name
                userImageView.sd_setImage(with: URL(string: _feedsData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))
                
                updateTimeAgo()
                postTextLabel.text = _feedsData.description
                upvoteBtn.setTitle(_feedsData.totalBull.toString(), for: .normal)
                upvoteBtn.setSelected(selected: _feedsData.isClickedBull, isAnimated: false)
                downvoteBtn.setSelected(selected: _feedsData.isClickedBear, isAnimated: false)
                downvoteBtn.setTitle(_feedsData.totalBear.toString(), for: .normal)
                commentbtn.setTitle(_feedsData.totalComments.toString(), for: .normal)
                optionsBtn.isHidden = !(_feedsData.canEdit && postData.canDelete)
            }
        }

Solution 2:[2]

The accepted solution is good, but not great.

This method's logic in particular needs to be improved:

func initialiseOutlet(_feedsData: feedsData) {
    nameLabel.text = _feedsData.userDetails.name
    userImageView.sd_setImage(with: URL(string: _feedsData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))

    updateTimeAgo()
    postTextLabel.text = _feedsData.description
    upvoteBtn.setTitle(_feedsData.totalBull.toString(), for: .normal)
    upvoteBtn.setSelected(selected: _feedsData.isClickedBull, isAnimated: false)
    downvoteBtn.setSelected(selected: _feedsData.isClickedBear, isAnimated: false)
    downvoteBtn.setTitle(_feedsData.totalBear.toString(), for: .normal)
    commentbtn.setTitle(_feedsData.totalComments.toString(), for: .normal)
    optionsBtn.isHidden = !(_feedsData.canEdit && postData.canDelete)
}

to something like this:

func configure(with viewModel: PostCellViewModel) {
    nameLabel.text = viewModel.username
    userImageView.sd_setImage(with: viewModel.userPhotoURL, placeholderImage: UIImage(named: "profile-image-placeholder"))

    updateTimeAgo()
    postTextLabel.text = viewModel.description
    upvoteBtn.setTitle(viewModel.totalBull, for: .normal)
    upvoteBtn.setSelected(selected: viewModel.isClickedBull, isAnimated: false)
    downvoteBtn.setSelected(selected: viewModel.isClickedBear, isAnimated: false)
    downvoteBtn.setTitle(viewModel.totalBear, for: .normal)
    commentbtn.setTitle(viewModel.totalComments, for: .normal)
    optionsBtn.isHidden = viewModel.isHidden
}

You are currently referencing postData and _feedsData (part of the Model) from Table View Cell - which is technically incorrect in the context of MVVM paradigm since View would have direct dependencies of Model...

Note that PostCellViewModel is the ViewModel struct (or class) you have to implement and it should look like this:

struct PostCellViewModel {
    private(set) var nameLabel: String
    private(set) var userImageURL: URL?
    // ...
    private(set) var postDescription: String
    private(set) var isHidden: Bool

    init(model: FeedItem) {
        nameLabel = model.userDetails.name
        userImageURL = URL(string: model.userDetails.photo)
        // ...
        postDescription = model.description
        isHidden = !(model.canEdit && model.post.canDelete)
    }
}

Depending on the project/team/coding standards, you may want to also use a protocol:

protocol PostCellViewModelType {
    var nameLabel: String { get }
    var userImageURL: URL? { get }
    // ...
    var postDescription: String { get }
    var isHidden: Bool { get }

    init(model: FeedItem)
}

And then implement it:

struct PostCellViewModel: PostCellViewModelType {
    private(set) var nameLabel: String
    private(set) var userImageURL: URL?
    // ...
    private(set) var postDescription: String
    private(set) var isHidden: Bool

    init(model: FeedItem) {
        // ...
    }
}

Also note that sd_setImage uses a library/pod/dependency, which on its turn uses functionality of the Networking/Service Layer. So probably it's better not to make Cell/View dependent on it. For cells' images in particular - you can add those calls inside cellForRow(at:), even if the method is implemented inside a dedicated UITableViewDatasource subclass and not inside the UIViewController directly.

For the UITableViewDatasource subclass, which is technically some controller/mediator type (since it depends on View and Model or ViewModel) - it's ok to interact with dependencies from other layers (Networking in case of image downloads). Views/Cells should care less if the image is to be downloaded or to to be fetched from local cache.

In general, if the images are too big and you want to implement a scalable architecture - you may want to create a custom ImageLoader class to take care of loading images only when needed, as well as canceling remote image requests if the cell disappears while the image download is in progress.

Have a look here for such a solution: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/

Also see how Apple recommends to implement a solution for a similar use-case: https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2