从MVC到VIPER,我踩过的iOS架构坑
去年双11前两周,我们组临时接到一个需求:把那个“祖传”的老商城App重构一遍。产品经理小王笑眯眯地说:“这次要支持动态化、模块化,还要上SwiftUI!”我当时一口老血差点喷出来——这代码还是2016年用Objective-C写的,ViewController动辄三千行,网络请求散落在各个角落,连单元测试覆盖率都不到5%。
作为团队里唯一一个从嵌入式转过来的Go开发(没错,就是那个天天和寄存器、中断打交道的硬件狗),我对“混乱”有着天然的敏感。在裸机上跑RTOS的时候,要是逻辑不清,分分钟死机给你看。如今写iOS,虽然不用再担心栈溢出把芯片烧了,但看到一坨巨型ViewController,心里还是会隐隐作痛。
所以,这次重构,我主动请缨牵头架构设计。领导拍了拍我肩膀:“行,你来定,但下个月必须上线。” 好家伙,Deadline压顶,还得兼顾产品迭代节奏,更别提最近在偷偷刷LeetCode准备跳槽了——这活儿干得好,简历上又能加一行“主导大型iOS应用架构升级”。
老古董MVC:又爱又恨的初恋
先说说我们原来用的MVC。Apple官方文档里吹得天花乱坠,结果呢?View和Controller紧紧耦合,Model形同虚设。用户点个按钮,ViewController既要处理UI刷新,又要拼URL、解析JSON、存本地缓存,甚至还要算促销折扣。这哪是Controller,分明是全能打工人!
// 典型MVC地狱现场
class ProductDetailViewController: UIViewController {
@IBOutlet weak var priceLabel: UILabel!
func loadProduct(id: String) {
// 1. 拼接URL
let url = "https://api.xxx.com/product?id=\(id)"
// 2. 发起网络请求(没用Alamofire,纯NSURLSession)
URLSession.shared.dataTask(with: URL(string: url)!) { data, _, _ in
// 3. 手动解析JSON
let json = try? JSONSerialization.jsonObject(with: data!)
// 4. 转成模型(没有Codable,全是字典取值)
let price = json?["price"] as? Double
// 5. 切回主线程更新UI
DispatchQueue.main.async {
self.priceLabel.text = "$\(price!)"
}
}.resume()
}
}
这种代码,在App Store审核时经常被揪出“性能问题”——主线程卡顿、内存泄漏。去年就有一次因为JSON解析崩溃,导致整个详情页打不开,被用户疯狂差评。审核团队还发邮件问:“你们的App是否经过充分测试?”
说实话,MVC不是不能用,但在复杂业务下,它就像用Arduino Uno去跑Linux内核——理论上可行,实际上自虐。
MVVM:解耦的希望,测试的福音
为了提升可测性(也为了让我能安心写单元测试,毕竟Go里TDD玩习惯了),我们决定试试MVVM。核心思想就一条:把ViewController变瘦,让ViewModel承担业务逻辑。
借助Swift的ObservableObject和@Published,配合SwiftUI,数据绑定变得异常优雅:
class ProductDetailViewModel: ObservableObject {
@Published var price: Double = 0.0
@Published var isLoading = false
private let productService: ProductServiceProtocol
init(productService: ProductServiceProtocol) {
self.productService = productService
}
func loadProduct(id: String) {
isLoading = true
Task {
do {
let product = try await productService.fetch(id: id)
await MainActor.run {
self.price = product.price
self.isLoading = false
}
} catch {
// 统一错误处理
print("Load failed: \(error)")
}
}
}
}
ViewController现在只负责“展示”,连网络请求都不碰了。测试?太简单了!Mock一个ProductServiceProtocol,就能覆盖所有分支逻辑。上周五晚上加班写测试用例,一口气跑了200+个case,零失败——那一刻,我仿佛回到了写Go服务时那种“稳如老狗”的感觉。
不过,MVVM也有坑。比如双向绑定容易造成循环引用,早期我们没注意,导致页面退出后ViewModel迟迟不释放,内存一路飙到200MB。还好 Instruments 的 Allocations 工具及时救场。
VIPER:重武器上阵,适合大项目
但产品野心越来越大:不仅要支持A/B测试,还要做插件化、支持多端复用。这时候,MVVM也开始显得力不从心——Presenter越来越臃肿,路由逻辑散落各处。
于是,我们瞄上了VIPER。这个架构把职责拆得极其细碎:View、Interactor、Presenter、Entity、Router,五层结构,每个模块高度内聚。听起来很重?确实重,但对于需要长期维护、多人协作的大型应用,VIPER就像给汽车装上了变速箱——初期调校麻烦,一旦跑起来,换挡丝滑,动力输出稳定。
举个例子,商品详情页的加载流程:
- View:只管UI事件(比如“加入购物车”点击)
- Presenter:接收View事件,协调业务逻辑
- Interactor:执行具体用例(如“获取商品信息”)
- Router:负责页面跳转(比如跳到支付页)
- Entity:纯数据模型
// Interactor负责业务用例
class ProductDetailInteractor {
weak var output: ProductDetailInteractorOutput?
private let repository: ProductRepositoryProtocol
func fetchProduct(id: String) {
Task {
do {
let product = try await repository.getProduct(id: id)
await MainActor.run {
output?.didFetchProduct(product)
}
} catch {
await MainActor.run {
output?.didFailWithError(error)
}
}
}
}
}
虽然代码量翻倍,但每个文件不超过200行,新人接手快,Code Review也轻松。更重要的是,模块之间通过协议通信,天然支持Mock和单元测试——这对想跳槽的我来说简直是简历镀金利器。
架构选型对比:别为了炫技而架构
最后,我整理了一张对比表,供大家在求职或实际项目中参考:
| 架构 | 适用场景 | 学习曲线 | 可测试性 | 代码量 | 团队协作 |
|---|---|---|---|---|---|
| MVC | 小型Demo、快速原型 | 低 | 差 | 少 | 难(易混乱) |
| MVVM | 中小型App、SwiftUI项目 | 中 | 好 | 中等 | 较好 |
| VIPER | 大型商业应用、长期维护项目 | 高 | 优秀 | 多 | 优秀 |
说到底,没有最好的架构,只有最合适的。如果你正在准备iOS岗位的面试,建议至少掌握MVVM,并了解VIPER的设计思想——很多大厂(比如我们合作的某东、某宝)都在用类似VIPER的Clean Architecture。
写在最后
这次重构上线后,崩溃率下降了70%,审核一次过,产品经理终于不再半夜打电话问我“为啥用户看不到价格”。而我,也在简历上添了浓墨重彩的一笔——上周刚收到猎头消息,有家公司看到我博客里写的“VIPER实战”,直接给了二面机会。
从嵌入式到Go,再到iOS,我始终相信:清晰的架构,就是程序员最好的防脱发药。毕竟,没人想在凌晨三点还在Debug一个三千行的ViewController,对吧?
所以,别再问“该用哪种架构”了。问问你的产品有多复杂,问问你的团队能维护多久,再问问你自己——明年跳槽时,这段经历能不能让你在面试官面前挺直腰板说一句:“我设计过可扩展、可测试、可维护的iOS架构。”

评论 0