1. 为什么 iOS 开发者需要 MVP,而不是把 ViewController 当垃圾桶
聊 MVP 之前,得先面对一个现实:iOS 开发里绝大多数项目,Controller 层都处于一种失控状态。网络请求在 Controller 里,数据解析在 Controller 里,UI 刷新逻辑在 Controller 里,甚至弹窗提示、空态判断、用户点击事件分发也全堆在 Controller 里。一个稍微复杂的页面,ViewDidLoad 里塞一千行代码是很常见的事。这种写法在项目初期完全没问题,因为页面简单、改动频繁、需求变化快,MVP 反而显得繁琐。但当页面进入第二个迭代周期,开始加状态、加埋点、加异常分支时,Controller 就会变成一座随时可能崩塌的危楼。
我参与过的一个电商项目就是典型案例。首页业务从最初的三四个接口扩展到十几个,首页 Controller 从两千行膨胀到五千多行。每次改需求,光是定位代码就要花很长时间。更难受的是,产品经理经常要求多个页面复用同样的逻辑——比如限时抢购的倒计时逻辑,首页有、商品详情页有、订单结算页也有。在传统 MVC 里,你只能把这套逻辑复制三份,或者写一堆难以维护的基类方法。直到有一天,一个同事改首页的倒计时逻辑时,不小心把详情页的开团逻辑也带崩了,我们才认真考虑引入 MVP。
MVP 的核心思想其实一句话就能说清:把视图和业务逻辑彻底拆开,让两者通过协议(Protocol)通信。比起 MVC 里的 Controller 既是中介又是业务执行者,MVP 里的 Presenter 是纯粹的业务逻辑输出单元,不持有任何 UIKit 相关的类。这意味着 Presenter 可以被独立测试、独立复用、独立替换,而 View 只是一个听话的展示工具。
在 iOS 的语境下,MVP 还有一个非常现实的价值:它是从 MVC 到 MVVM 的过渡站。MVVM 里的 ViewModel 用 KVO 或 Combine 做绑定,很多刚转 Swift 的团队会不习惯;MVP 的 Presenter 则完全用代理回调,上手成本低得多。如果你现在的项目还在用 Objective-C,MVP 几乎是重构 Controller 的最优解,因为 OC 的 KVO 写起来太痛苦,而代理是 OC 开发者的肌肉记忆。
下面这套内容,是我在几个真实项目里实践 MVP 后总结的完整方案,包含代码落地、内存管理、数据绑定、复杂页面拆分的全套经验。适合已经会写基础 iOS 页面、但对架构还没有清晰思路的开发者,也适合项目已经膨胀、正准备做技术重构的团队。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MVP 三件套的角色边界:Model、View、Presenter 各管什么
要真正用好 MVP,首先得把三个角色的边界画清楚。很多文章只讲"MVP 是 Model-View-Presenter",但真到自己写代码时,最迷惑的就是"这个逻辑到底该放 Presenter 还是 View?""这个状态是 Model 还是 Presenter 的?"。
2.1 View 只做两件事:展示数据和转发事件
MVP 里的 View 通常指 ViewController + View + Cell,但我的建议是,用协议 ViewInterface 抽象出一个"可被 Presenter 驱动的视图",然后 ViewController 实现这个协议。View 的职责是纯粹的 UI 表现层:
- 接收 Presenter 给的 viewModel 对象,把它们渲染到界面控件上
- 把用户点击、滚动手势、页面生命周期事件转交给 Presenter
View 不应该做任何业务判断,不要写 if 判断网络状态,不要计算价格是否凑满减,不要字典转模型。这些逻辑全部向上抛给 Presenter。View 里允许保留的只有纯粹的 UI 操作,比如收起键盘、滚动到指定位置、控制控件显隐。
这里容易踩一个坑:很多人把"刷新列表"和"决定刷新哪一行"混在一起。刷新列表(reloadData)是 View 的事,但决定是刷新整个列表还是局部刷新、刷新后显示空态页还是加载失败页,这是 Presenter 的职责。View 只负责执行刷新动作,至于为什么刷新、刷新成什么样,它不关心。
2.2 Model 不只是一个数据类,它要能承载业务规则
MVP 里的 Model 容易被人简化成"一个 JSON 对应的数据模型"。理论上这样能用,但会不小心把业务逻辑塞进 Presenter 或 ViewController。
更合理的 Model 设计是:数据模型 + 业务规则模型。
数据模型很简单,就是 Codable/YYModel 解析出来的结构体,放着商品名、价格、库存这些字段。业务规则模型则负责纯粹的领域逻辑,比如计算商品是否可售、根据优惠策略计算实际支付金额、判断当前是否处于抢购窗口期。这些规则不依赖 UIKit,可以独立单测。
拿电商项目举例子,优惠金额计算逻辑我强烈建议放进 Model 层而不是 Presenter:
objc复制// 商品模型 + 价格计算规则
@interface ProductModel : NSObject
@property (nonatomic, copy) NSString *productId;
@property (nonatomic, assign) double originalPrice;
@property (nonatomic, assign) double discountPrice;
// 是否可售
- (BOOL)isAvailable;
// 根据优惠策略计算最终价格
- (double)finalPriceWithCoupon:(CouponModel *)coupon;
@end
这样 Presenter 只是把这些规则的结果拿过来拼装成 viewModel,而不是自己重写一遍价格计算逻辑。以后优惠策略变了,只动 Model,Presenter 和 View 都不用改。
2.3 Presenter 是唯一的业务决策中心
Presenter 是整个模式的核心,它接收 View 转发来的事件,调用 Model 层的数据请求或业务规则,处理完成后把结果包装成 viewModel,通过协议回调给 View。
Presenter 里禁止出现任何 import UIKit 的代码。不能直接给 UILabel 赋值,不能 push 一个新的 ViewController(那是 View 或 Router 干的活),不能在回调里使用 weak self 去操作界面——它只能通过代理方法告诉 View"你应该展示这些数据了"。
我见过太多"伪 MVP"代码,Presenter 里写 self.viewController.titleLabel.text = @"xxx",或者直接 #import "GoodsDetailViewController.h" 然后 push。这样写还不如不引入 MVP,因为你把 View 的实现细节泄漏到了业务层,一旦 UI 调整,业务层也得跟着改。
2.4 通信规则:View 和 Presenter 的双向代理,Model 对两者无感
MVP 的通信机制是双向代理。View 持有 Presenter 对象,Presenter 通过 ViewInterface 协议反向驱动 View。Model 是被动数据源,它的变化要么由 Presenter 主动拉取,要么通过回调上报给 Presenter。
这里给出一个协议定义示例:
objc复制// 视图协议:Presenter 通过它驱动 View 更新
@protocol GoodsListViewInterface <NSObject>
- (void)showLoading;
- (void)hideLoading;
- (void)reloadGoodsList:(NSArray<GoodsCellViewModel *> *)viewModels;
- (void)showEmptyView;
- (void)showErrorViewWithMessage:(NSString *)message tip:(NSString *)tip;
@end
// Presenter 协议:View 通过它向 Presenter 传递事件
@protocol GoodsListPresenterInterface <NSObject>
- (void)viewDidLoad;
- (void)didPullToRefresh;
- (void)didReachBottom;
- (v
