Swift MVVM架构实战:列表数据展示与交互完整指南
摘要
基于MVVM架构,使用原生Swift实现列表数据展示与交互功能,涵盖数据渲染、单元格复用、点
Swift 基于MVVM架构实现完整列表数据展示与交互功能实战案例
一、架构简介
MVVM 在 iOS 开发圈子里已经算是主流标配了。跟传统的 MVC 相比,它把职责拆得更清楚:View 只管页面长什么样、用户怎么点;ViewModel 负责业务逻辑、数据处理和视图状态的管理;Model 就老老实实承载数据模型。这么一拆,视图和业务逻辑就彻底解耦了,代码可读性、可维护性,甚至单元测试的友好度,都上了一个台阶。对于列表类这种常规业务来说,MVVM 几乎是天然适配的。

这篇就以一个基础列表页为例,从零开始,把数据渲染、单元格复用、点击交互、数据刷新这些核心功能一一走通。全程用原生 Swift,适配 iOS 14+,不依赖第三方框架。
二、项目结构
目录划分清晰,严格遵循 MVVM 的分层逻辑:
├── Model // 数据模型层
├── ViewModel // 业务逻辑层
├── View // 视图层(控制器、自定义Cell)
三、代码实现
3.1 数据模型(Model)
先建一个列表数据源模型。采用 Codable 协议,方便后续网络数据解析。这里定义列表展示所需的几个核心字段:
import Foundation
// 列表数据模型
struct ListModel: Codable {
let id: Int
let title: String
let desc: String
}
3.2 视图模型(ViewModel)
ViewModel 是整个架构的枢纽层。它封装了数据请求、数据数组管理、单元格数据赋值、点击事件回调——但绝不持有任何视图对象。通过闭包来回调数据刷新和点击事件,视图和逻辑之间就彻底松绑了。
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)
ListCell 封装了 UITableViewCell,只负责两件事:UI 布局和数据赋值。不处理任何业务逻辑,ViewModel 把数据传过来,它照单显示就行。
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 去处理,控制器只管“接活儿”。
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)
}
}
四、功能测试与总结
运行效果很直观:项目启动后,页面异步加载模拟数据,列表自动渲染;点击任意单元格,弹出提示弹窗并打印日志,单元格正常取消选中状态。整个交互流程一气呵成。
架构上的优势也一目了然:Model 只管数据,不带任何视图代码;ViewModel 独立处理数据请求和业务逻辑,可以单独做单元测试;View 只专注 UI 展示。代码分层清晰,后期维护和扩展都省心。
如果后续项目需要扩展,可以在这个基础上继续加:下拉刷新、上拉加载更多、网络异常处理、空页面占位图等通用功能,适配更复杂的业务场景完全没问题。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。