如何结合协议与MVVM实现UITableView多类型自定义单元格?
用协议+MVVM实现UITableView自定义单元格配置
嘿,我来帮你梳理下怎么基于你现有的代码,用协议+MVVM模式来实现UITableView不同行的自定义单元格配置。这种方式能很好地解耦视图、视图模型和控制器,让代码更易维护和扩展~
第一步:定义通用协议,统一配置规则
首先我们需要两个核心协议:一个用来规范单元格的配置行为,另一个用来定义视图模型需要提供的单元格元数据(比如标识符、高度)。这样不管是哪种单元格或视图模型,都能遵循统一的规则交互。
import UIKit // 通用单元格配置协议:所有自定义Cell都要遵循,负责接收ViewModel并更新UI protocol ConfigurableCell: UITableViewCell { associatedtype ViewModelType func configure(with viewModel: ViewModelType) } // 通用CellViewModel协议:定义每个单元格的基础属性,控制器靠它来获取单元格的标识符和高度 protocol CellViewModel { var cellIdentifier: String { get } var cellHeight: CGFloat { get } } // 针对文本类单元格的ViewModel协议(可以根据需求扩展其他类型,比如带输入框的、带图片的) protocol TextPresentableCellViewModel: CellViewModel { var text: String { get } }
第二步:实现具体的视图模型
基于你现有的ProfileNameViewModel,我们扩展它来遵循新的协议,同时新增ProfileEmailViewModel来对应第二行的需求:
struct ProfileNameViewModel: TextPresentableCellViewModel { // 提供当前ViewModel对应的Cell标识符 var cellIdentifier: String { return "ProfileNameCell" } // 自定义单元格高度 var cellHeight: CGFloat { return 60 } // 你原本的文本内容 var text: String { return "Name" } } struct ProfileEmailViewModel: TextPresentableCellViewModel { var cellIdentifier: String { return "ProfileEmailCell" } var cellHeight: CGFloat { return 60 } var text: String { return "Email" } }
第三步:改造自定义Cell,遵循配置协议
让你的UITableViewCell实现ConfigurableCell协议,这样它就能接收对应的ViewModel并自动更新UI。如果不同Cell的UI逻辑不同,也可以单独实现:
// 姓名单元格 class ProfileNameCell: UITableViewCell, ConfigurableCell { @IBOutlet weak var titleLabel: UILabel! // 实现协议方法,用ViewModel配置UI func configure(with viewModel: TextPresentableCellViewModel) { titleLabel.text = viewModel.text // 还可以在这里添加其他UI配置,比如字体、颜色等 } } // 邮箱单元格(如果UI和姓名Cell一致,也可以复用同一个Cell类,这里分开是为了演示扩展性) class ProfileEmailCell: UITableViewCell, ConfigurableCell { @IBOutlet weak var titleLabel: UILabel! func configure(with viewModel: TextPresentableCellViewModel) { titleLabel.text = viewModel.text titleLabel.textColor = .systemBlue // 可以给邮箱行设置不同的样式 } }
第四步:更新控制器,用ViewModel数组管理数据源
改造你的ProfileController,让它持有一个CellViewModel数组作为数据源,这样控制器不用关心具体的单元格类型,只需要根据ViewModel提供的信息来复用和配置单元格:
class ProfileController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! // 用统一的CellViewModel数组管理所有行的数据 private var cellViewModels: [CellViewModel] = [] override func viewDidLoad() { super.viewDidLoad() setupTableView() loadCellData() } private func setupTableView() { // 注册自定义Cell(如果用xib的话) tableView.register(UINib(nibName: "ProfileNameCell", bundle: nil), forCellReuseIdentifier: "ProfileNameCell") tableView.register(UINib(nibName: "ProfileEmailCell", bundle: nil), forCellReuseIdentifier: "ProfileEmailCell") tableView.dataSource = self tableView.delegate = self } private func loadCellData() { // 按顺序添加不同的ViewModel,对应TableView的每一行 cellViewModels.append(ProfileNameViewModel()) cellViewModels.append(ProfileEmailViewModel()) tableView.reloadData() } // MARK: - UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cellViewModels.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let viewModel = cellViewModels[indexPath.row] // 根据ViewModel提供的标识符获取Cell let cell = tableView.dequeueReusableCell(withIdentifier: viewModel.cellIdentifier, for: indexPath) // 类型转换并配置Cell:确保Cell和ViewModel类型匹配 if let textVM = viewModel as? TextPresentableCellViewModel, let configurableCell = cell as? any ConfigurableCell where configurableCell.ViewModelType == TextPresentableCellViewModel.self { configurableCell.configure(with: textVM) } return cell } // MARK: - UITableViewDelegate func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { // 从ViewModel获取高度,不用在控制器里硬编码 return cellViewModels[indexPath.row].cellHeight } }
为什么这么做?
- 解耦性强:控制器不用关心具体的Cell和ViewModel类型,只需要处理通用的协议交互;Cell和ViewModel一一对应,各自负责自己的逻辑。
- 扩展性好:如果要新增一行(比如“Phone”),只需要新增一个
ProfilePhoneViewModel和对应的ProfilePhoneCell(如果需要),不用修改控制器的核心代码,符合开闭原则。 - 测试友好:ViewModel是纯结构体,易于单元测试,不用依赖UI组件。
内容的提问来源于stack exchange,提问作者decipher
相关产品推荐
相关产品推荐

