Swift 基于MVVM架构实现完整列表数据展示与交互功能实战案例

Swift 基于MVVM架构实现完整列表数据展示与交互功能实战案例

一、架构简介

MVVM 是 iOS 开发中主流的架构模式,在传统 MVC 基础上做了职责拆分:View 负责页面视图展示与用户交互、ViewModel 承担业务逻辑、数据处理与视图状态管理、Model 仅承载数据模型。该架构实现视图与业务逻辑解耦,代码可读性、可维护性和单元测试性大幅提升,非常适合列表类常规业务开发。

本文以基础列表页为例,从零实现数据渲染、单元格复用、点击交互、数据刷新等核心功能,全程使用原生 Swift 语法,适配 iOS 14+ 系统。

二、项目结构

整体目录划分清晰,严格遵循 MVVM 分层:

arduino 复制代码
├── Model        // 数据模型层
├── ViewModel    // 业务逻辑层
├── View         // 视图层(控制器、自定义Cell)

三、代码实现

3.1 数据模型(Model)

创建列表数据源模型,采用 Codable 协议,方便后续网络数据解析,定义列表展示所需字段。

swift 复制代码
import Foundation

// 列表数据模型
struct ListModel: Codable {
    let id: Int
    let title: String
    let desc: String
}

3.2 视图模型(ViewModel)

ViewModel 作为中间层,封装数据请求、数据数组管理、单元格数据赋值、点击事件回调,不持有任何视图对象。使用闭包回调数据刷新与点击事件,解耦视图与逻辑。

swift 复制代码
import Foundation

class ListViewModel {
    // 数据源数组
    private(set) var dataArray: [ListModel] = []
    // 数据刷新回调
    var reloadDataClosure: (() -> Void)?
    // 单元格点击回调
    var cellClickClosure: ((ListModel) -> Void)?
    
    // 模拟请求本地/网络数据
    func requestListData() {
        // 模拟异步网络请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in
            guard let self = self else { return }
            // 构造测试数据
            let tempData = [
                ListModel(id: 1, title: "Swift基础语法", desc: "Swift数据类型、函数与流程控制讲解"),
                ListModel(id: 2, title: "UIKit控件使用", desc: "UILabel、UIButton、UITableView实战"),
                ListModel(id: 3, title: "MVVM架构思想", desc: "架构分层与解耦设计原则")
            ]
            self.dataArray = tempData
            // 主线程回调刷新视图
            DispatchQueue.main.async {
                self.reloadDataClosure?()
            }
        }
    }
    
    // 获取单个单元格数据
    func getCellModel(index: Int) -> ListModel? {
        guard index >= 0, index < dataArray.count else { return nil }
        return dataArray[index]
    }
    
    // 触发单元格点击事件
    func didSelectRow(index: Int) {
        guard let model = getCellModel(index: index) else { return }
        cellClickClosure?(model)
    }
}

3.3 自定义单元格(View)

封装 UITableViewCell,仅负责 UI 布局与数据赋值,不处理业务逻辑,接收 ViewModel 传递的数据进行展示。

swift 复制代码
import UIKit

class ListCell: UITableViewCell {
    // UI控件
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
        return label
    }()
    
    private let descLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 13)
        label.textColor = .gray
        label.numberOfLines = 0
        return label
    }()
    
    // 初始化布局
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 页面布局
    private func setupUI() {
        contentView.addSubview(titleLabel)
        contentView.addSubview(descLabel)
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        descLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            
            descLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
            descLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
            descLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
            descLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12)
        ])
    }
    
    // 绑定数据
    func configData(model: ListModel) {
        titleLabel.text = model.title
        descLabel.text = model.desc
    }
}

3.4 主控制器(View)

控制器作为视图载体,仅负责创建视图、绑定 ViewModel 回调、响应页面交互,所有业务逻辑交由 ViewModel 处理。

swift 复制代码
import UIKit

class ListViewController: UIViewController {
    // 初始化ViewModel
    private let viewModel = ListViewModel()
    private let tableView = UITableView()
    private let cellID = "ListCell"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBase()
        setupTableView()
        bindViewModel()
        // 发起数据请求
        viewModel.requestListData()
    }
    
    private func setupBase() {
        view.backgroundColor = .white
        title = "MVVM列表实战"
    }
    
    // 初始化表格
    private func setupTableView() {
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.frame = view.bounds
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(ListCell.self, forCellReuseIdentifier: cellID)
        tableView.tableFooterView = UIView()
    }
    
    // 绑定ViewModel回调
    private func bindViewModel() {
        // 数据刷新
        viewModel.reloadDataClosure = { [weak self] in
            self?.tableView.reloadData()
        }
        // 单元格点击
        viewModel.cellClickClosure = { model in
            print("点击条目:\(model.title),ID:\(model.id)")
            let alert = UIAlertController(title: "点击提示", message: model.title, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "确定", style: .default))
            self.present(alert, animated: true)
        }
    }
}

// UITableView 代理与数据源
extension ListViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.dataArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as? ListCell,
              let model = viewModel.getCellModel(index: indexPath.row) else {
            return UITableViewCell()
        }
        cell.configData(model: model)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        viewModel.didSelectRow(index: indexPath.row)
    }
}

四、功能测试与总结

  1. 运行效果:启动项目后,页面会异步加载模拟数据,自动渲染列表;点击任意单元格,弹出提示弹窗并打印日志,单元格正常取消选中状态。
  2. 架构优势:Model 只负责数据存储,无任何视图相关代码;ViewModel 独立处理数据请求、业务逻辑,可单独做单元测试;View 仅专注 UI 展示,代码分层清晰。
  3. 扩展方向:可在此基础上拓展下拉刷新、上拉加载更多、网络异常处理、空页面占位图等通用功能,适配复杂业务场景。

海量精选技术文档和实战案例持续更新,敬请关注【风骏时光少年】公众号

相关推荐
就叫_这个吧2 小时前
JavaScript基础数据类型、运算符、数组、函数的定义及DOM方式应用
开发语言·前端·javascript
作业逆流成河2 小时前
别再一次性重构枚举了:如何把一个真实后台项目的状态字典,渐进式迁移到enum-plus?
前端·javascript·开源
暗不需求2 小时前
React 性能优化秘籍:深入理解 `useMemo` 与 `useCallback`
前端·react.js·面试
专注VB编程开发20年2 小时前
我制作excel工作簿的选项卡,发给deep seek, 昨天修改了一天
前端·vue.js·excel
light blue bird2 小时前
工序路径主子表单工序组装图表组件
前端·数据库·信息可视化·.net·web端·razor page
linlinlove22 小时前
前端uniapp、后端thinkphp股票系统开发功能展示、代码披露、HQChart
前端·uni-app·echarts·thinkphp·hqchart·配资·deepseek选股票
万少2 小时前
Claude Code 任务结束会自己喊你:一个 Stop Hook 搞定提示音
前端·后端·代码规范
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_30:(玩转列表样式,从基础到进阶)
前端·css·html·tensorflow·媒体
ct9782 小时前
TypeScript 中的泛型
前端·javascript·typescript