8. MVC在移动应用开发中的应用:以iOS的MVC模式为例
移动端开发,说白了就是跟屏幕尺寸、触摸交互、还有碎片化的生命周期打交道。我做了这么多年iOS开发,发现很多新手一上来就堆代码,Controller里塞了几千行,最后自己都看不懂。嗯,这其实就是没用好MVC。
今天咱们就拿iOS来聊聊,MVC在移动端到底该怎么落地。我个人的经验是,iOS的MVC虽然被吐槽“太重”,但只要你把边界划清楚,它依然是中小型项目最稳的架构。
8.1 iOS中的MVC角色划分
在iOS里,MVC的三个角色跟经典定义略有不同。苹果官方文档其实写得很清楚,但很多人没仔细看。
| 角色 | iOS对应组件 | 核心职责 |
|---|---|---|
| Model | NSObject / Codable 结构体 | 数据存储、业务逻辑、网络请求结果 |
| View | UIView / Storyboard / XIB | 界面展示、用户交互响应 |
| Controller | UIViewController | 协调Model与View、处理生命周期、事件转发 |
你可能会问:那UIViewController到底算不算Controller?算,但也不完全是。我在项目中遇到过一种情况:把网络请求、数据解析、UI刷新全塞进ViewController,结果一个文件三千行。这其实已经违背了MVC的初衷——Controller应该只做“协调”,而不是“包办”。
8.2 一个典型的iOS MVC示例
咱们来看一个用户列表的案例。假设我们要从网络获取用户数据,然后展示在表格里。
Model层
struct User: Codable {
let id: Int
let name: String
let email: String
}
class UserService {
func fetchUsers(completion: @escaping ([User]) -> Void) {
// 模拟网络请求
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let users = [
User(id: 1, name: "张三", email: "zhangsan@example.com"),
User(id: 2, name: "李四", email: "lisi@example.com")
]
DispatchQueue.main.async {
completion(users)
}
}
}
}
这里我把数据模型和网络服务都归为Model层。有人可能会把网络请求放到Controller里,我个人习惯是坚决不——Controller一旦掺和网络逻辑,测试就变得非常痛苦。
View层
class UserCell: UITableViewCell {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
func configure(with user: User) {
nameLabel.text = user.name
emailLabel.text = user.email
}
}
View只负责展示,不关心数据从哪来。这个Cell拿到User对象,直接往Label上填就行。我曾经见过有人直接在Cell里写网络请求,那画面太美我不敢看。
Controller层
class UserListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var users: [User] = []
private let userService = UserService()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
loadUsers()
}
private func loadUsers() {
userService.fetchUsers { [weak self] users in
self?.users = users
self?.tableView.reloadData()
}
}
}
extension UserListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as! UserCell
cell.configure(with: users[indexPath.row])
return cell
}
}
Controller在这里只做了三件事:触发加载、接收数据、刷新UI。它不直接操作网络,也不直接操作Cell的Label。说白了,它就是个“中间人”。
8.3 MVC在移动端的常见陷阱
我刚开始用MVC做iOS开发时,踩过不少坑。这里列几个最常见的,你对照看看自己有没有中招。
- Massive ViewController——这是iOS MVC被黑得最惨的地方。Controller里塞了太多东西,导致难以维护。解决办法:把网络请求抽到Service,把数据转换抽到ViewModel(虽然这有点MVVM的味道,但实用主义至上)。
- View直接持有Model——有些新手直接在UIView里写一个var user: User,然后在View里处理业务逻辑。这会导致View和Model耦合,换数据源时得改View代码。正确做法是View只暴露配置方法,由Controller来调用。
- 忽略Model的独立性——Model应该是一个纯粹的Swift结构体或类,不依赖UIKit。我见过有人把UIImage写到Model里,结果单元测试跑不起来。记住:Model只关心数据,不关心展示。
8.4 MVC在iOS中的通信机制
MVC三个角色之间怎么通信?这是很多人的盲区。我画了一张图,帮你理清关系。
从图上你能看到,View和Model永远不直接通信。所有的交互都经过Controller。这其实是个好设计——哪天你想把UITableView换成UICollectionView,只需要改View层,Model完全不用动。
8.5 实际项目中的MVC优化
说实话,纯MVC在大型项目里确实会有点吃力。我一般会在MVC基础上做两点优化:
- 引入Service层——把网络请求、数据库操作、文件读写都封装到Service里。Controller只调用Service,不关心具体实现。这样单元测试时,Mock一个Service就行。
- 使用Notification或KVO做松耦合——当Model发生变化时,通过Notification广播给多个Controller。比如用户登录状态变化,多个页面都需要刷新,用Notification比层层传递方便得多。
但要注意,不要滥用Notification。我曾经在一个项目里用了二十多个Notification,结果调试时根本不知道谁发了谁收了。后来我改用RxSwift,但那是另一个话题了。
8.6 总结
iOS的MVC模式,说白了就是把界面、数据、协调三者拆开。你想想看,如果你把这三样混在一起,改UI时可能影响数据,改数据时可能搞崩界面。拆开之后,每个部分都可以独立测试、独立修改。
我个人的经验是:MVC不是银弹,但它是最容易上手、最容易理解的架构。对于中小型iOS项目,MVC完全够用。如果你发现Controller开始膨胀了,别急着换架构,先试试把业务逻辑抽到Model层,把网络请求抽到Service层。很多时候,问题就解决了。
嗯,今天就聊到这儿。记住:架构是为业务服务的,别为了用架构而用架构。