skip to Main Content

I am working on an app that uses TableView for showing feeds to users using ViewModel and my ViewModel contains a variable that contains data of all cells and ViewModel also contains other data as well, what I am doing is passing the 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 the data array? thanks.

2

Answers


  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)
                }
            }
    
    Login or Signup to reply.
  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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search