TableView 单元格中的 MVVM

MVVM in TableView Cell

希望你一切都好,我正在开发一个应用程序,它使用 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 引用和索引传递给单元格然后每个单元格从数据数组访问其数据是正确的还是好的方法? (如果不是请指导我)。 非常感谢。

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

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

查看模型

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

TableView 单元格

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 结构(或 class),它应该如下所示:

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

根据 project/team/coding 标准,您可能还想使用协议:

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 使用 library/pod/dependency,后者又使用 Networking/Service 层的功能。所以最好不要让 Cell/View 依赖它。 特别是对于单元格的图像 - 您可以在 cellForRow(at:) 内添加这些调用,即使该方法是在专用的 UITableViewDatasource subclass 内实现的,而不是在 UIViewController 直接.

对于 UITableViewDatasource subclass,从技术上讲,它是某种 controller/mediator 类型(因为它取决于视图和模型或 ViewModel)- 可以与来自其他层的依赖项交互(在图像下载的情况下是网络)。 Views/Cells 应该不太关心图像是要下载还是要从本地缓存中获取。

一般来说,如果图像太大并且您想要实现可扩展的架构 - 您可能需要创建自定义 ImageLoader class 以仅在需要时负责加载图像,以及取消如果在图片下载过程中单元格消失,则远程图片请求。

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

另请参阅 Apple 建议如何针对类似 use-case 实施解决方案: https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views