You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何结合协议与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:35:22