简体   繁体   English

TableView 单元格中的 MVVM

[英]MVVM in TableView Cell

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:我正在开发一个应用程序,该应用程序使用TableView向使用ViewModel的用户显示提要,我的 ViewModel 包含一个包含所有单元格数据的变量,ViewModel 也包含其他数据,我正在做的是将整个 ViewModel 引用和 indexPath 传递给细胞,在这里你可以看到:

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?那么,将完整的 ViewModel 引用和索引传递给单元格,然后每个单元格从数据数组访问其数据是正确的还是好的方法? thanks.谢谢。

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...您目前正在从 Table View Cell 引用postData_feedsData (模型的一部分)——这在 MVVM 范例的上下文中在技术上是不正确的,因为 View 将具有 Model 的直接依赖性...

Note that PostCellViewModel is the ViewModel struct (or class) you have to implement and it should look like this:请注意, PostCellViewModel是您必须实现的 ViewModel 结构(或类),它应该如下所示:

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.另请注意, sd_setImage使用库/pod/依赖项,而后者又使用网络/服务层的功能。 So probably it's better not to make Cell/View dependent on it.所以最好不要让 Cell/View 依赖它。 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.特别是对于单元格的图像 - 您可以在cellForRow(at:)中添加这些调用,即使该方法是在专用的 UITableViewDatasource 子类中实现的,而不是直接在 UIViewController 中实现的。

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).对于 UITableViewDatasource 子类,它在技术上是某种控制器/中介类型(因为它依赖于 View 和 Model 或 ViewModel)- 可以与其他层的依赖项进行交互(在图像下载的情况下是网络)。 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.一般来说,如果图像太大并且你想实现一个可扩展的架构——你可能想要创建一个自定义的 ImageLoader class 来处理只在需要时加载图像,以及如果单元格消失时取消远程图像请求图片下载正在进行中。

Have a look here for such a solution: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/在这里查看这样的解决方案: 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另请参阅 Apple 建议如何针对类似用例实施解决方案: https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views

*Passing whole ViewModel reference and indexPath to cell is not necessary. *不需要将整个 ViewModel 引用和 indexPath 传递给单元格。 Call back after receiving data:收到数据后回调:

ViewController -> ViewModel -> TableViewDatasource -> TableViewCell.* 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查看 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)
            }
        }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM