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
希望你一切都好,我正在开发一个应用程序,它使用 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