TableView 单元格中的 MVVM

我正在开发一个应用程序,该应用程序使用 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)
}

那么,将完整的 ViewModel 引用和索引传递给单元格,然后每个单元格从数据数组中访问其数据是正确还是好方法?谢谢。

stack overflow MVVM in TableView Cell
原文答案
author avatar

接受的答案

*不需要将整个 ViewModel 引用和 indexPath 传递给单元格。收到数据后回调:

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

视图控制器

 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()
                }
           })
        }
    }

查看模型

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 数据源

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

表格视图单元格

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)
            }
        }

答案:

作者头像

公认的解决方案很好,但不是很好。

该方法的逻辑尤其需要改进:

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)
}

像这样:

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
}

您当前正在从 Table View Cell 中引用 postData_feedsData (模型的一部分)——这在 MVVM 范式的上下文中在技术上是不正确的,因为 View 将直接依赖于模型......

请注意, 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)
    }
}

根据项目/团队/编码标准,您可能还需要使用协议:

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

    init(model: FeedItem)
}

然后实现它:

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) {
        // ...
    }
}

另请注意, sd_setImage 使用库/pod/依赖项,而后者又使用网络/服务层的功能。所以可能最好不要让 Cell/View 依赖它。特别是对于单元格的图像 - 您可以在 cellForRow(at:) 中添加这些调用,即使该方法是在专用的 UITableViewDatasource 子类中实现的,而不是直接在 UIViewController 中实现的。

对于 UITableViewDatasource 子类,它在技术上是某种控制器/中介类型(因为它依赖于 View 和 Model 或 ViewModel) - 可以与来自其他层的依赖项交互(在图像下载的情况下是网络)。如果要下载图像或从本地缓存中获取图像,视图/单元格应该不太关心。

通常,如果图像太大并且您想要实现可扩展的体系结构 - 您可能希望创建一个自定义 ImageLoader 类来处理仅在需要时加载图像,以及如果单元格消失而取消远程图像请求,而图片下载正在进行中。

在这里查看这样的解决方案: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/

另请参阅 Apple 建议如何为类似用例实施解决方案: https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views