'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 |