前言
在iOS开发中,随着项目规模的扩大,传统的MVC模式往往会导致控制器臃肿、业务逻辑耦合严重、测试困难。为了解决这些问题,通过引入ViewModel分担部分逻辑,从而减轻控制器的负担,这就是MVVM模式。
然而在业务快速迭代的场景下,ViewModel往往逐渐演变成"臃肿版本的VM"------逻辑依旧集中、扩展和维护成本依然偏高。
这时,源自Clean Architecture 的VIPER 就值得被重新考虑。它通过更细粒度的职责拆分(View、Interactor、Presenter、Entity、Router) ,将UI、业务、状态和路由解耦,从而在可维护性、可测试性与扩展性上提供了更清晰的边界。
问题是,VIPER的多数文章与教程停留在理论或Demo阶段,真正落地到生产项目时,往往因为生命周期管理复杂、依赖链混乱、Router边界模糊而陷入困难。
本文将基于真实项目中的实践经验,分享一套从Demo到生产可落地的VIPER工程化方案,涵盖依赖注入、响应式绑定、生命周期管理等核心要素,并通过一个复杂信息流页面的案例,展示VIPER如何支撑复杂业务的演进。
一、VIPER概念与为什么难落地
1.1 VIPER相关概念
VIPER 是源自Clean Architecture在iOS中的一种实现方式。它强调单一职责,将功能模块拆分为五个部分:
- View:负责UI展示以及用户交互的处理,不涉及业务逻辑处理。
- Interactor:业务逻辑核心层,负责数据获取、处理加工、执行业务规则。
- Presenter:作为中间层,更像一个"协调员",协调View与Interactor,将业务数据转化为 UI 状态。
- Entity:纯数据模型,承载业务实体或数据结构。
- Router:负责模块之间的跳转与依赖管理,提供导航逻辑。
通过这种拆分,VIPER 将UI、业务逻辑、数据、导航分离开,使得每一层职责更加清晰,也让单元测试变得更容易。

如图所示,VIPER通过明确关系 和数据流向 实现了各层的分离职责。实线表示持有关系 ,虚线表示通信或者回调,确保了单向数据流和依赖倒置原则的实现。
1.2 为什么VIPER难以落地?
虽然VIPER概念看起来优雅,但在工程实践中却面临诸多挑战,相信很多同学都尝试过,但往往停留在Demo阶段,难以在复杂业务中真正落地。主要原因如下:
1. 生命周期与依赖管理复杂
传统MVC/MVVM架构中,生命周期相对简单,主要集中在Controller或ViewModel上。而VIPER将职责分散到五个组件中,每个组件都有自己的生命周期,组件间的依赖关系更加复杂。
常见问题:
- 缺乏合理的依赖注入机制,容易产生循环引用
- 生命周期管理混乱、对象销毁时期不明确,影响功能的同时导致内存泄漏
2. 各层职责边界模糊
理论上各层职责很清晰,但实践中边界往往变得模糊。比如:将UI状态转换逻辑写入Interactor,违背了业务逻辑纯净性,又或者Presenter承担过多协调工作,导致出现"臃肿的Presenter"。
3. 状态管理机制缺失
iOS原生缺乏统一的状态管理机制,在复杂交互场景下Presenter容易陷入回调地狱,状态切换逻辑充斥着大量if-else判断,异步操作时状态跨层同步困难
4. Router层设计困境
在简单Demo中的Router往往只做页面跳转,但真实业务场景下需要处理:
- 跨模块导航的参数传递与依赖注入
- 模块复用与动态创建逻辑
- 深层级路由的状态恢复
- 定制下的自定义转场动画
- 权限控制与导航拦截
如果这些问题处理不当,Router层会变得很难用并难以控制。
4. 工程化与团队协作成本
编码阶段,VIPER需要编写大量模板代码,团队需要配备完善的代码生成工具和统一的编码规范来保证一致性。同时,该架构学习曲线陡峭,新成员上手成本较高。在实际项目中,当面临紧迫的开发周期和业务压力时,团队往往更倾向于选择更简单直接的架构方案,VIPER的复杂性反而成为了推广的阻碍。
二、面向协议编程与设计原则
2.1 为什么选择面向协议编程
VIPER本质上是面向协议编程的一种具体实践,传统的继承体系在复杂业务场景下暴露出明显弊端。
传统继承方式的问题:单继承限制、强耦合、测试困难。
协议驱动的优势:
- 多协议组合:一个类可以遵循多个协议,实现功能的组合式设计
- 依赖倒置:高层模块依赖抽象协议,而非具体实现
- 易于测试:可以轻松创建遵循协议的mock对象
我们以Presenter层设计为例:
objc
@protocol TJPViperBasePresenterProtocol <NSObject>
#pragma mark - 获取数据
/**
* 获取分页数据
* @param page 页码
* @param success 成功回调
* @param failure 失败回调
*/
- (void)fetchInteractorDataForPage:(NSInteger)page
success:(void(^)(NSArray *data, NSInteger totalPage))success
failure:(void(^)(NSError *error))failure;
#pragma mark - 信号绑定
/**
* 绑定Interactor的页面跳转信号
* @param contextProvider controller上下文
*/
- (void)bindInteractorToPageSubjectWithContextProvider:(id<TJPViperBaseViewControllerProtocol>)contextProvider;
/**
* 绑定Interactor的数据更新信号
*/
- (void)bindInteractorDataUpdateSubject;
/**
* presenter层透传刷新信号
*/
@property (nonatomic, strong) RACSubject<NSDictionary *> *viewUpdatedDataSignal;
@end
这种设计让View层只需要依赖协议接口,完全不关心Presenter接口的具体实现,实现了真正的解耦。
2.2 协议粒度的设计原则
在协议设计时,在理论层面我们只需要考虑依赖倒置原则或单一职责原则即可,但在实际开发过程中需要平衡功能完整性和职责单一性。
粗粒度协议:包含完整的业务流程,适合对外暴露接口,比如上述设计的PresenterProtocol。
细粒度协议 :职责单一,便于组合和测试,但需要注意粒度拆分过细会增加复杂度。
个人结论:在实践中,比较通用的方式是尽量遵循单一职责的前提下,采用粗粒度协议或者组合式协议设计。
objc
@protocol TJPExampleDataFetchProtocol <NSObject>
- (void)fetchDataForPage:(NSInteger)page completion:(void(^)(id result, NSError *error))completion;
@end
@protocol TJPExamplePaginationProtocol <NSObject>
- (NSInteger)getCurrentPage;
- (BOOL)hasMoreData;
@end
@protocol TJPExampleViperBasePresenterProtocol <TJPExampleDataFetchProtocol, TJPExamplePaginationProtocol, TJPLifecycleProtocol>
// 通过协议组合实现功能完整性
@end
如代码所示,我们可以细粒度进行单测,对外暴露接口使用组合协议,但缺点是稍微复杂。因此在本项目中,我选择粗粒度协议设计,将相关功能整合到单一协议中,并进行一定的冗余设计,主要原因是:
- 降低学习成本,更容易理解单一协议的完整功能
- 简化依赖管理,减少了协议依赖的复杂性
- 避免过多小协议导致的接口管理混乱
2.3 单向数据流的实现
VIPER强调单向数据流,避免双向绑定带来的混乱状态,如图所示:

通过协议定义清晰的数据流向:
objc
// View -> Presenter:用户操作
@protocol TJPViperBaseViewControllerProtocol <NSObject>
- (UIViewController *)currentViewController;
- (void)showError:(NSString *)error;
@end
// Interactor -> Presenter:数据回流
@protocol TJPViperBaseInteractorProtocol <NSObject>
/// 数据源更新需求
@property (nonatomic, strong) RACSubject *dataListUpdatedSignal;
/// 透传跳转需求
@property (nonatomic, strong) RACSubject *navigateToPageSubject;
@end
这种设计确保了数据流的可预测性和调试性。
三、构建VIPER基础框架
3.1 分层骨架抽象设计
View层基础组件,因为篇幅原因代码都为节选,下同。
objc
@interface TJPViperBaseTableViewController : UIViewController <TJPViperBaseViewControllerProtocol>
// 核心组件
@property (nonatomic, strong) TJPBaseTableView *tableView;
//vc->强引用presenter
@property (nonatomic, strong) id<TJPViperBasePresenterProtocol> basePresenter;
// 状态管理
@property (nonatomic, strong, readonly) TJPViewControllerStateMachine *stateMachine;
/// 是否启用下拉刷新
@property (nonatomic, assign) BOOL shouldEnablePullDownRefresh;
/// 是否启用上拉加载更多
@property (nonatomic, assign) BOOL shouldEnablePullUpRefresh;
@end
设计亮点:
- 状态机管理 :通过状态机(TJPViewControllerStateMachine) 统一管理页面状态,避免状态混乱
- 可配置化:通过BOOL标志控制功能开关,支持不同页面需求
- 强类型协议:通过协议定义清晰的接口边界
Presenter层设计
objc
@interface TJPViperBasePresenterImpl : NSObject <TJPViperBasePresenterProtocol>
// 核心组件
//presenter->强引用Interactor和router 防止提前释放
@property (nonatomic, strong) id<TJPViperBaseInteractorProtocol> baseInteractor;
@property (nonatomic, strong) id<TJPViperBaseRouterHandlerProtocol> baseRouter;
// 此处用weak (避免循环引用)
@property (nonatomic, weak) id<TJPViperBaseViewControllerProtocol> contextProvider;
// 错误处理
@property (nonatomic, strong, readonly) TJPViperDefaultErrorHandler *errorHandler;
@end
设计亮点:
- 内存管理:Presenter强引用Interactor、Router防止提前释放,此处的contextProvider为View的协议接口,使用弱引用避免循环引用
Interactor层设计
objc
@interface TJPViperBaseInteractorImpl : NSObject <TJPViperBaseInteractorProtocol>
// 基础控件
@property (nonatomic, strong, readonly) TJPCacheManager *cacheManager;
@property (nonatomic, strong, readonly) TJPViperDefaultErrorHandler *errorHandler;
// 网络配置
@property (nonatomic, assign) NSTimeInterval requestTimeout;
@property (nonatomic, assign) NSInteger maxRetryCount;
// 状态管理
@property (nonatomic, assign, readonly) BOOL isInitialized;
// 分页管理
@property (nonatomic, strong, readonly) TJPPaginationInfo *currentPagination;
@property (nonatomic, assign, readonly) NSInteger currentPage;
@property (nonatomic, assign, readonly) BOOL hasMoreData;
@property (nonatomic, assign) NSInteger defaultPageSize;
@end
设计亮点:
- 单一职责原则:Interactor基本为数据获取、业务逻辑处理、缓存等业务侧相关功能。
3.2 状态管理机制
状态机实现
objc
typedef NS_ENUM(NSInteger, TJPViewControllerState) {
TJPViewControllerStateIdle, // 空闲
TJPViewControllerStateInitialLoading, // 初始加载
TJPViewControllerStateContent, // 有内容
TJPViewControllerStateRefreshing, // 刷新中
TJPViewControllerStateLoadingMore, // 加载更多
TJPViewControllerStateEmpty, // 空状态
TJPViewControllerStateError // 错误状态
};
- (void)stateMachine:(TJPViewControllerStateMachine *)stateMachine
didTransitionFromState:(TJPViewControllerState)fromState
toState:(TJPViewControllerState)toState {
[self updateUIForState:toState withData:self.dataArray];
}
设计优势:
- 状态集中管理:所有状态都经过状态机,便于调试和维护
- UI更新自动化:状态变化自动触发UI更新,减少手动管理
- 状态转换验证:可添加状态转换前后的条件检查
3.3 请求防重与错误处理
请求防重复机制
objc
@property (nonatomic, strong) NSMutableSet<NSNumber *> *activeRequests;
- (void)loadDataForPage:(NSInteger)page {
NSNumber *pageKey = @(page);
if (self.shouldPreventDuplicateRequests &&
[self.activeRequests containsObject:pageKey]) {
return;
}
[self.activeRequests addObject:pageKey];
// 执行请求...
}
统一错误处理
objc
- (void)handleDataFetchError:(NSError *)error forPage:(NSInteger)page {
[self.errorHandler handleError:error inContext:self completion:^(BOOL shouldRetry) {
if (shouldRetry) {
[self fetchDataForPage:page];
} else {
[self.stateMachine transitionToState:TJPViewControllerStateError];
}
}];
}
四、VIPER与注入式框架Typhoon结合
4.1 依赖注入概念及必要性
依赖注入(Dependency Injection, DI) 是后端开发特别重要的概念,尤其在Java生态中几乎是标配。
依赖(Dependency):一个类需要另一个类来完成工作, 比如Person类需要Water类,那会在需要的时候new一个Water类。
依赖注入(DI):把对象的创建交给外部容器或框架来完成,被依赖的对象以"参数"形式注入到类中,而不是自己去[alloc init]。
传统VIPER实现中,各层组件的创建和依赖关系管理往往通过硬编码实现,这带来以下问题:
- 紧耦合:组件间直接依赖,难以替换和测试
- 配置分散:依赖关系散布在各处,维护困难
- 环境切换困难:开发、测试、生产环境的依赖配置差异大
Typhoon是较早出现的一种依赖注入框架,具有高度的可定制性,整体成熟且稳定。但由于其设计背景偏向Objective-C时代,在当今以Swift为主流的环境下显得略有不适配。
注入方式选择
构造函数注入:依赖关系明确,对象创建后不可变
objc
// 示例代码
- (instancetype)initWithInteractor:(id<TJPViperBaseInteractorProtocol>)interactor
router:(id<TJPViperBaseRouterHandlerProtocol>)router {
if (self = [super init]) {
_baseInteractor = interactor; // 不可变,生命周期内固定
_baseRouter = router;
}
return self;
}
属性注入:配置灵活,支持可选依赖
objc
// 示例代码
@property (nonatomic, strong) id<TJPViperBaseInteractorProtocol> baseInteractor;
方法注入:一般用的较少,通常用于特殊场景
objc
// 示例代码
- (void)configureWithInteractor:(id<TJPViperBaseInteractorProtocol>)interactor {
_baseInteractor = interactor;
[self initializeInteractor];
}
4.2 Typhoon集成方案
Assembly配置详见仓库中代码,部分代码如下:
objc
// 示例代码
@implementation TJPModuleAssembly
- (id<TJPViperBasePresenterProtocol>)basePresenter {
return [TyphoonDefinition withClass:[TJPViperBasePresenterImpl class]
configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(baseInteractor) with:[self baseInteractor]];
[definition injectProperty:@selector(baseRouter) with:[self baseRouter]];
}];
}
在Typhoon框架中,Assembly是框架核心之一, 它就像是一个容器配置类, 可以理解为"依赖关系的配置中心"。这个配置类主要有以下作用:
- 告诉Typhoon容器,某个类的实例如何构建
- 创建该类时需要哪些依赖
- 所需要的这些依赖从哪里能获取到
生命周期管理
objc
// 单例:适用于无状态的服务类 由Typhoon管理的单例,容器只创建一次并复用
definition.scope = TyphoonScopeSingleton;
// 原型:适用于有状态的业务对象 每次请求都会创建新的实例
definition.scope = TyphoonScopePrototype;
// 弱单例:内存压力时可被回收
definition.scope = TyphoonScopeWeakSingleton;
该属性为生命周期标识符。在实际使用中需要注意TyphoonScopeSingleton 和常见的单例创建方式(dispatch_once)是冲突的,因此需要做取舍。如果是手写了单例,就不需要TyphoonScopeSingleton,否则会出现两个不同的管理机制,造成混乱的同时会造成异常状态问题。
主意:如果不显示指定scope,默认为TyphoonScopeObjectGraph,介于Prototype和Singleton之间。不会全局复用,但在一条解析链中不会重复创建。
循环依赖处理
objc
// 示例代码
@implementation TJPModuleAssembly
- (id<TJPViperBasePresenterProtocol>)basePresenter {
return [TyphoonDefinition withClass:[TJPViperBasePresenterImpl class]
configuration:^(TyphoonDefinition *definition) {
// 延迟注入避免循环依赖
[definition injectProperty:@selector(baseInteractor)
with:[TyphoonInjectionByReference definitionReferenceWithKey:@"baseInteractor"]];
}];
}
@end
Typhoon处理循环依赖主要为两个关键点:两阶段注入机制 和至少一侧用属性注入(类似于打破循环引用机制)。
两阶段注入机制:
- 先把对象创建出来,但不填充依赖,将实例放进容器的"集合",为了防止重复创建。
- 根据injectProperty或injectParameter去解析并注入依赖。
至少一侧用属性注入:
- 如果用构造函数注入,此时需要再构造函数里传入依赖。如果A构造函数依赖B,B构造函数依赖A,就会造成死锁问题。
- 正确做法是一边用构造函数,另一边用属性注入避免死锁问题发生。
实际开发过程中,个人建议非必要不使用构造函数传入依赖,统一使用属性注入,可以防止死锁问题。
五、VIPER与响应式框架结合
5.1 为什么要在VIPER中使用RAC
传统的VIPER实现中,各层件通信往往依赖Delegate、Block或Notification,这些方式在简单场景下没有任何问题,甚至非常便捷,但在复杂业务场景下会导致回调地狱、状态同步困难、内存管理复杂等等。
回调地狱
objc
// 示例代码
[self.interactor fetchDataWithSuccess:^(NSArray *data) {
[self.presenter processData:data completion:^(NSArray *processedData) {
[self.view updateUI:processedData completion:^{
[self.router checkNavigationNeeded:processedData callback:^(BOOL shouldNavigate) {
if (shouldNavigate) {
// 又一层回调...
}
}];
}];
}];
} failure:^(NSError *error) {
// 错误处理...
}];
使用RAC带来的改善
objc
// RAC链式操作示例代码
[[[[self.interactor fetchDataSignal takeUntil:self.rac_willDeallocSignal] flattenMap:^RACSignal *(NSArray *data) {
return [self.presenter processDataSignal:data];
}] deliverOnMainThread] subscribeNext:^(NSArray *processedData) {
[self.view updateUIWithData:processedData];
} error:^(NSError *error) {
[self.view showError:error];
}];
使用takeUntil自动管理订阅生命周期,统一的错误处理机制,filter、map、merge等操作符丰富。
5.2 VIPER中的信号设计
由于VIPER强调单向数据流 :从View→Presenter→Interactor→Data。在这条正向数据链中,方法调用遵循直接调用 方式。这样能够保持层与层之间的依赖清晰,调用路径简单明了,详见代码中的fetchInteractorDataForPage方法。
而在反向数据传输链(从Interactor→Presenter→View)中,采用RAC信号机制来传递数据,通过设计不同类型信号实现业务结果的解耦传递,便捷地驱动UI更新。代码片段如下:
objc
// Interactor层代码片段
@protocol TJPViperBaseInteractorProtocol <NSObject>
// 跳转需求信号
@property (nonatomic, strong) RACSubject *navigateToPageSubject;
// 数据源更新信号
@property (nonatomic, strong) RACSubject<NSDictionary *> *dataListUpdatedSignal;
@end
@implementation TJPViperBaseInteractorImpl
- (void)createData:(NSDictionary *)data completion:(void (^)(id _Nullable, NSError * _Nullable))completion {
TJPLOG_INFO(@"[%@] 创建数据: %@", NSStringFromClass([self class]), data);
// ...省略业务规则代码
// 发送数据更新信号
[self.dataListUpdatedSignal sendNext:@{@"action": @"create", @"data": result}];
});
}
// 懒加载信号
- (RACSubject<NSDictionary *> *)dataListUpdatedSignal {
if (!_dataListUpdatedSignal) {
_dataListUpdatedSignal = [RACSubject subject];
}
return _dataListUpdatedSignal;
}
@end
实际业务中,Interactor层通过业务规则验证并执行数据操作后,将返回的结果通过信号发送给上层即Presenter层。
objc
// Presenter层代码片段
@protocol TJPViperBasePresenterProtocol <NSObject>
// 透传信号
@property (nonatomic, strong) RACSubject<NSDictionary *> *viewUpdatedDataSignal;
@end
@implementation TJPViperBasePresenterImpl
- (void)bindInteractorDataUpdateSubject {
if (!self.baseInteractor) {
TJPLOG_ERROR(@"无法绑定数据更新信号: interactor 为空");
return;
}
@weakify(self)
[[self.baseInteractor.dataListUpdatedSignal takeUntil:self.rac_willDeallocSignal] subscribeNext:^(NSDictionary * _Nullable x) {
@strongify(self)
TJPLOG_INFO(@"[%@] 接收到来自Interactor的数据更新信号", NSStringFromClass([self class]));
[self.viewUpdatedDataSignal sendNext:x];
}];
}
// 懒加载信号
- (RACSubject<NSDictionary *> *)viewUpdatedDataSignal {
if (!_viewUpdatedDataSignal) {
_viewUpdatedDataSignal = [RACSubject subject];
}
return _viewUpdatedDataSignal;
}
@end
Presenter层在收到数据后在通过信号透传至View层。
objc
// View层代码片段
@implementation TJPViperBaseTableViewController
- (void)bindInteractorSignals {
[self.basePresenter bindInteractorToPageSubjectWithContextProvider:self];
// 绑定数据更新信号
[self.basePresenter bindInteractorDataUpdateSubject];
// throttle防抖动处理
@weakify(self)
[[[[[self.basePresenter viewUpdatedDataSignal] takeUntil:self.rac_willDeallocSignal] throttle:0.3] deliverOnMainThread] subscribeNext:^(NSDictionary * _Nullable updateDict) {
TJPLOG_INFO(@"[TJPViperBaseTableViewController] VC层收到Interactor透传过来的数据源更新信号");
@strongify(self)
if (updateDict && self.isInitialized) {
dispatch_async(dispatch_get_main_queue(), ^{
[self childVCUpdateDatasource:updateDict];
});
}
}];
}
@end
通过Interactor层发送信号 、Presenter层透传信号 、View层订阅信号来驱动View的更新,即可完成单向数据流中的反向传输链路。这样即保持了层间解耦,也简化了UI更新逻辑。
六、生命周期管理与依赖链
6.1 VIPER组件生命周期设计
在VIPER架构中,五个核心组件(View、Presenter、Interactor、Entity、Router)各自拥有独立的生命周期。
正确管理它们的创建、初始化、工作与销毁阶段,是防止内存泄漏和状态混乱的关键。
标准生命周期流程
- 创建阶段
- 由DI容器或工厂方法负责创建实例,保证每个组件的实例化与职责边界保持一致,避免随意在 View 内直接[alloc init]。
- 注入阶段
- 建立组件间依赖关系,比如View注入Presenter;Presenter注入Interactor、Router。
- 初始化阶段
- 执行必要的初始化逻辑,例如:Presenter初始化业务状态,Interactor配置缓存、分页信息。在此阶段,不建议发起耗时操作,以免阻塞首屏加载。
- 绑定阶段
-
建立信号订阅和数据流:Presenter绑定Interactor的数据更新信号,View绑定Presenter的状态输出信号
-
注意事项:此处极易产生循环引用,必须通过weak-strong dance或RACDisposable来管理。
- 工作阶段
- 组件进入正常运行:View响应用户交互并转发给Presenter,Presenter调度,Interactor执行业务,Router负责页面跳转。
- Entity 在此阶段作为纯数据载体被频繁读写。
- 清理阶段
- 当View即将销毁或业务完成时,需要显式清理:Presenter解除所有信号绑定,释放Interactor;Interactor停止数据订阅、清空缓存、关闭网络请求;Router清理临时导航状态。
- 销毁阶段
- 对象被ARC回收或由容器移除。
6.2 依赖链设计原则
持有关系设计原则:
- View强持有Presenter:确保Presenter生命周期
- Presenter强持有Interactor和Router:确保业务层和路由层不被提前释放
- Presenter弱持有View:避免循环引用
- Router强持有依赖注入容器:支持动态创建其他模块
七、工程化实践:如何减少重复代码并保持一致性
7.1 为什么要有基础类+接口
在实际项目中,VIPER各层都存在大量重复的基础逻辑,如果让每个具体实现都从头开始编写,会导致以下问题:
- 大量代码重复,相同的生命周期管理、错误处理逻辑分散
- 不同开发者风格迥异,代码风格一致性很难保证,后期维护困难
- 迭代成本高,框架升级时需要修改所有具体实现
定义基础类负责框架基本逻辑,使用Protocol定义接口规范,子类可以选择集成Base类或直接实现Protocol,Base类定义通用处理规则,子类实现自己独有的业务逻辑,后续新功能通过Protocol接口扩展,不会影响现有代码。
7.2 CellModel驱动的TableView实现
传统的TableView实现中,cellForRowAtIndexPath方法往往包含大量的类型判断代码,难以维护。通过CellModel驱动的方式,让CellModel决定Cell类型和高度,TableView 只负责展示:
objc
@protocol TJPBaseCellModelProtocol <NSObject>
// 跳转信号
@property (nonatomic, strong) RACCommand<id<TJPBaseCellModelProtocol>, NSObject*>* selectedCommand;
// Cell类,用于注册
- (Class)cellName;
// Cell高度
- (CGFloat)cellHeight;
@end
具体CellModel实现示例
objc
@interface TJPImageCellModel : TJPBaseCellModel
@property (nonatomic, copy) NSString *imageId;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray<NSString *> *imageUrls;
@property (nonatomic, assign) NSInteger likes;
@property (nonatomic, assign) NSInteger comments;
@property (nonatomic, copy) NSString *imageDescription;
@end
@implementation TJPImageCellModel
- (NSString *)cellName {
return @"TJPImageCell";
}
- (CGFloat)cellHeight {
return 280.0f;
}
@end
配套的TableView代码片段
objc
// 自动注册Cell类型
- (void)registerCellsForDataArray:(NSArray *)dataArray {
NSMutableSet *registeredIdentifiers = [NSMutableSet set];
for (id<TJPBaseCellModelProtocol> cellModel in dataArray) {
NSString *identifier = [cellModel cellIdentifier];
if (![registeredIdentifiers containsObject:identifier]) {
Class cellClass = [cellModel cellClass];
[self.tableView registerClass:cellClass forCellReuseIdentifier:identifier];
[registeredIdentifiers addObject:identifier];
}
}
}
// 展示cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section >= self.internalSections.count) {
TJPLOG_WARN(@"[TJPBaseTableView] section越界: %ld", indexPath.section);
return [self defaultErrorCell:@"section 越界"];
}
id<TJPBaseSectionModelProtocol> sectionModel = self.internalSections[indexPath.section];
if (indexPath.row >= sectionModel.cellModels.count) {
TJPLOG_WARN(@"[TJPBaseTableView] row越界: %ld", indexPath.row);
return [self defaultErrorCell:@"row 越界"];
}
id<TJPBaseCellModelProtocol> model = sectionModel.cellModels[indexPath.row];
NSString *cellIdentifier = [model cellName];
UITableViewCell<TJPBaseTableViewCellProtocol> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
TJPLOG_WARN(@"[TJPBaseTableView] 找不到注册的cell: %@", cellIdentifier);
return [self defaultErrorCell:@"未注册cell"];
}
if ([cell respondsToSelector:@selector(configureWithModel:)]) {
[(id)cell configureWithModel:model];
}
return cell;
}
// 动态高度计算
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
id<TJPBaseSectionModelProtocol> section = self.internalSections[indexPath.section];
id<TJPBaseCellModelProtocol> model = section.cellModels[indexPath.row];
return model.cellHeight;
}
// Cell选择事件分发
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
id<TJPBaseSectionModelProtocol> section = self.internalSections[indexPath.section];
id<TJPBaseCellModelProtocol> model = section.cellModels[indexPath.row];
TJPLOG_INFO(@"第 %ld 行被选中,模型: %@", (long)indexPath.row, model);
if (model.selectedCommand) {
[model.selectedCommand execute:model];
}
if (self.tjpBaseTableViewDelegate && [self.tjpBaseTableViewDelegate respondsToSelector:@selector(tjpTableView:didSelectRowAtIndexPath:)]) {
[self.tjpBaseTableViewDelegate tjpTableView:tableView didSelectRowAtIndexPath:indexPath];
}
}
7.3 模板方法模式的应用
在VIPER架构 中,模板方法模式(Template Method Pattern) 被广泛应用,用于定义模块的基础骨架和执行流程规则,而将具体的业务逻辑延迟到子类实现。
以Presenter层为例,基类通过模板方法定义了通用的执行流程,子类只需重写其中的关键步骤,从而填充差异化的业务逻辑。
这种设计方式的优势在于:
- 减少重复代码:通用逻辑集中在基类,避免不同 Presenter 重复实现相同的框架代码。
- 保持流程一致性:骨架由基类控制,确保模块行为规范统一。
- 扩展灵活:子类只需关注自身差异化逻辑,扩展和维护更加简单。
代码片段如下:
objc
@implementation TJPViperBasePresenterImpl
- (void)fetchInteractorDataForPageWithPagination:(NSInteger)page success:(void (^)(NSArray * _Nonnull, TJPPaginationInfo * _Nullable))success failure:(void (^)(NSError * _Nonnull))failure {
// 参数校验 详见代码
// 预处理请求
[self preprocessRequestForPage:page];
// 标记请求开始
[self.activeRequests addObject:pageKey];
self.isProcessingRequest = YES;
// 逻辑处理方法 详见仓库代码
}
#pragma mark - 子类可重写的业务逻辑方法
- (void)preprocessRequestForPage:(NSInteger)page {
TJPLOG_INFO(@"[%@] 正在预处理第 %ld 页的请求", NSStringFromClass([self class]), (long)page);
// 子类可重写
}
- (void)postprocessResponseData:(NSArray *)data {
TJPLOG_INFO(@"[%@] 正在后处理 %lu 条响应数据", NSStringFromClass([self class]), (unsigned long)data.count);
// 子类可重写
}
- (BOOL)validateResponseData:(NSArray *)data {
if (!data) {
return NO;
}
return YES; // 子类可重写进行更详细的验证
}
- (void)handleBusinessError:(NSError *)error {
TJPLOG_ERROR(@"[%@] 发生业务错误: %@", NSStringFromClass([self class]), error.localizedDescription);
// 子类可重写
}
#pragma mark - 子类可重写的扩展方法
- (void)setupPresenter {
// 子类可重写此方法进行初始化设置
}
- (void)teardownPresenter {
// 子类可重写此方法进行清理工作
}
@end
通过模板方法模式,我们既能在基类中统一管理公共逻辑,又能在子类中灵活扩展差异化行为,大幅提升了代码复用性与可维护性。
7.4 代码生成工具的探索
在当前的 iOS 开发生态中,针对VIPER架构的成熟代码生成工具仍然相对缺乏。然而,代码生成对于提升团队开发效率具有重要意义。
现状与问题
由于VIPER强调严格的分层和协议抽象,一个完整的模块通常至少需要4--5个文件(View、Presenter、Interactor、Router、Assembly等),这会带来一些实际问题:
- 重复工作多:大量重复的协议声明和基础实现需要手工编写。
- 容易遗漏:生命周期方法、依赖绑定等关键代码在手写过程中容易遗漏,导致运行时错误。
可行的探索方向
虽然目前iOS生态社区内没有广泛使用的"标准工具",但已有一些通用代码生成框架可以用于探索和实践Generamba就是一个典型选择,它允许团队基于模板快速生成符合项目规范的代码骨架。
下面是一个简化的示例模板配置,用于生成自定义的 VIPER 模块文件:
yaml
# Template配置文件
name: "tjp_viper_module"
summary: "TJP VIPER module template"
author: "Your Team"
version: "1.0"
license: "MIT"
code_files:
- {name: View/TJPViewControllerProtocol.h, path: xxxx/Code/View/view_protocol.h.liquid}
- {name: View/TJPViewController.h, path: xxxx/Code/View/view.h.liquid}
- {name: View/TJPViewController.m, path: xxxx/Code/View/view.m.liquid}
- {name: Presenter/TJPPresenter.h, path: xxxx/Code/Presenter/presenter.h.liquid}
- {name: Presenter/TJPPresenter.m, path: xxxx/Code/Presenter/presenter.m.liquid}
即便没有完善的代码生成工具,通过规范的Base类设计和清晰的模板方法模式,也能显著降低VIPER的使用门槛和维护成本。
八、复杂页面案例实战
演示目标:构建一个信息流页面,包含 新闻 / 图片集 / 视频 / 用户动态 / 商品 / 广告 等多种 Cell,完成分页、缓存 、响应式交互 、路由跳转的"生产级页面"闭环。
演示素材:

8.1 层级职责
- View层:只负责渲染UI组件、转发交互事件;UI状态由状态机控制。
- Presenter层(TJPVIPERDemoPresenter) :协调业务流转,经Router处理跳转逻辑
- Interactor层(TJPVIPERDemoInteractorImpl) :获取数据、构建CellModel、维护分页与缓存策略,通过信号将数据更新通知给上层
- Entity: TJPFeedResponse、TJPPaginationInfo、TJPProductCellModel等数据实体
- Router: 跳转路由,支持[alloc init]方式创建的VC,以及DI方式创建的VC
8.2 分页与缓存策略
在TJPVIPERDemoInteractorImpl 中,统一通过模板方法performDataRequestForPage:withPagination: 获取数据。因为分页不同业务会采用不同策略,TJPPaginationInfo支持游标分页 与页码分页两种模式。
objc
- (TJPPaginationInfo *)createPaginationInfoFromResponse:(TJPFeedResponse *)response forPage:(NSInteger)page {
// 根据实际业务需求选择分页方式
BOOL useCursorPagination = [self shouldUseCursorPaginationForPage:page];
if (useCursorPagination) {
// 游标分页方式
return [self createCursorBasedPagination:response];
} else {
// 页码分页方式
return [self createPageBasedPagination:response];
}
}
- (BOOL)shouldUseCursorPaginationForPage:(NSInteger)page {
// 这里可以根据业务逻辑决定使用哪种分页方式
return page > 3;
}
关键点:
- TJPPaginationInfo抽象两种分页模型,Presenter层与View层无需关心分页类型变化
- parametersForPage 会根据currentPagination.paginationType方法自动拼装分页所需属性字段
- 缓存采用SWR原则:命中则先回显缓存,再根据数据变动规则进行缓存校准
8.3 多类型Cell的模型装配
数据源源自本地JSON,模拟真实请求环境,再统一转换为对应的CellModel ,并打包为Section返回:
objc
NSArray *cellModels = [self convertFeedsToModels:feedResponse.feeds];
TJPBaseSectionModel *section = [[TJPBaseSectionModel alloc] initWithCellModels:cellModels];
completion(@[section], paginationInfo, nil);
针对不同类型的Feed流数据,分别生成对应的CellModel模型,例如:
objc
- (TJPNewsCellModel *)createNewsModelFromDict:(NSDictionary *)dict {
TJPNewsCellModel *m = [TJPNewsCellModel new];
m.newsId = dict[@"id"];
m.title = dict[@"title"];
m.summary = dict[@"summary"];
m.imageUrl = dict[@"imageUrl"];
m.selectedCommand = self.selectedNewsCommand; // 绑定命令
return m;
}
8.4 响应式交互:从点击信号至路由跳转
每个CellModel 会实现一个selectedCommand 。在Interactor 层中统一实现六类命令 (新闻/图片/视频/动态/商品/广告),它们的signalBlock只做一件事------把选中的模型经navigateToPageSubject信号往上游传递。
代码片段如下:
objc
// 创建信号
_selectedNewsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(TJPNewsCellModel *input) {
// 发送跳转信号
[self.navigateToPageSubject sendNext:input];
return [RACSignal empty];
}];
// Presenter层代码
- (TJPNavigationModel *)buildNewsNavigationModel:(id)cellModel {
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
if ([cellModel respondsToSelector:@selector(newsId)]) {
id newsId = [(NSObject *)cellModel valueForKey:@"newsId"];
if (newsId) parameters[@"newsId"] = newsId;
}
if ([cellModel respondsToSelector:@selector(title)]) {
id title = [(NSObject *)cellModel valueForKey:@"title"];
if (title) parameters[@"title"] = title;
}
TJPNavigationModel *model = [TJPNavigationModel modelWithRouteId:@"newsDetail" parameters:[parameters copy] routeType:TJPNavigationRouteTypeViewPush];
model.animated = YES;
return model;
}
// Router代码 TJPVIPERDemoRouter.m文件
- (UIViewController *)createViewControllerForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters {
// 根据策略选择创建方式
TJPRouterCreationStrategy strategy = [self creationStrategyForRoute:routeId];
switch (strategy) {
case TJPRouterCreationStrategyDI:
return [self createViewControllerWithDI:routeId parameters:parameters];
case TJPRouterCreationStrategyHardcode:
return [self createViewControllerWithHardcode:routeId parameters:parameters];
case TJPRouterCreationStrategyFactory:
return [self createViewControllerWithFactory:routeId parameters:parameters];
default:
NSLog(@"[ViperDemoRouter] 未知的创建策略: %ld", (long)strategy);
return nil;
}
}
/**
* DI方式创建ViewController
* 通过Typhoon等DI框架创建,适用于复杂的VIPER模块
*/
- (UIViewController *)createViewControllerWithDI:(NSString *)routeId parameters:(NSDictionary *)parameters {
// VIPER Demo主页
if ([routeId isEqualToString:@"viperDemo"]) {
return [self.viperModuleProvider viperDemoViewController];
}
// VIPER Demo详情页
if ([routeId isEqualToString:@"viperDemoDetail"]) {
UIViewController *detailVC = [self.viperModuleProvider viperDemoDetailViewController];
detailVC.hidesBottomBarWhenPushed = YES;
return detailVC;
}
// 通过DI创建的新闻详情页
if ([routeId isEqualToString:@"newsDetail"]) {
// 获取Title
NSString *title = [parameters objectForKey:@"title"];
return [self.viperModuleProvider viperNewsDetailViewControllerWithTitle:title];
}
return nil;
}
除了基本的Cell → Presenter → Router跳转链,Router 还做了进一步的工程化设计:
- 标准接口 :通过TJPViperBaseRouterHandlerProtocol统一定义路由跳转的职责(参数验证、目标VC创建、依赖注入、跳转前后钩子)。
- 默认实现 :TJPViperBaseRouterImpl提供了通用的模板方法,子类只需关注具体路由逻辑,比如如何创建目标 VC。
- 协调器模式 :通过TJPNavigationCoordinator统一分发路由请求,支持多种跳转方式(Push、Present、DI 创建等),避免Router变得过于臃肿。
- 可扩展性:支持参数预处理、依赖注入、统计埋点等横切逻辑,方便业务团队扩展。
这样一来,Router 不仅仅是"页面跳转器",而是一个可扩展、可插拔的导航系统,为大型项目中的跨模块协作提供了更强的灵活性。
8.5 状态机与列表UI协作
在这个案例中,页面状态的管理交给了TJPViewControllerStateMachine状态机。 它的职责是统一维护页面生命周期的各种状态,并让View层(VC)只关注如何渲染。
典型状态流转:
- 首屏加载:InitialLoading状态 → 请求成功进入Content内容显示状态,若无数据则进入Empty空数据状态;
- 下拉刷新:Refreshing状态;
- 上拉加载更多:LoadingMore状态;
- 请求失败:Error状态(配合统一ErrorHandler提示,并可一键重试)。
这种设计有两个好处:
- 提高可维护性:新增状态时只需要在状态机枚举中补齐,转换规则增加新状态的转换逻辑即可。
- 状态与渲染解耦:VC只需要关心"在某个状态如何绘制"即可。
代码如下:
objc
- (void)updateUIForState:(TJPViewControllerState)state withData:(nullable id)data {
dispatch_async(dispatch_get_main_queue(), ^{
switch (state) {
case TJPViewControllerStateInitialLoading:
[self showInitialLoadingState];
break;
case TJPViewControllerStateContent:
[self showContentState:data];
break;
case TJPViewControllerStateRefreshing:
// 刷新状态下不需要额外UI更新,刷新控件会自动显示
break;
case TJPViewControllerStateLoadingMore:
// 加载更多状态下不需要额外UI更新
break;
case TJPViewControllerStateEmpty:
[self showEmptyState];
break;
case TJPViewControllerStateError:
[self showErrorState:data];
break;
default:
break;
}
});
}
8.6 在生产页面中的完整工作流
在复杂业务的实践中,VIPER通过数据流+交互链串联起来。结合实际案例,梳理完整工作流程如下:
1. 数据请求链(View触发 → Presenter调度 → Interactor执行)
VC只负责触发加载(初始化数据、下拉、上拉)不参与业务细节,Presenter负责调度请求,真正的数据获取在 Interactor层。Interactor发起网络请求,负责分页抽象与切换。相关核心方法为createPaginationInfoFromResponse:、parametersForPage:。
2. 数据建模链(数据响应 → CellModel → Section)
Interactor将原始数据转换为多种CellModel (新闻/图片/视频/用户动态/商品/广告),再组装为Section 。相关核心方法为createModelFromDict。
3. 状态更新链(Presenter透传 → 状态机状态变更 → 驱动UI渲染)
Presenter将数据透传,状态机将结果转为"状态事件"(Content/Empty/Error/Refreshing/LoadingMore)。
View层使用配套的TableView,通过CellModel展示不同的Cell,它不用关心具体的Cell是什么,只负责展示即可。
UI更新的两条路径:
- 常规数据返回后的刷新:状态机进入Content状态,VC执行reloadDataWithSectionModels: 。
- 增量数据流下的局部更新:Presenter通过viewUpdatedDataSignal 信号推送增量事件,VC重写childVCUpdateDatasource: 精准更新(reloadRows/performBatchUpdates)。
4. 路由跳转链(Cell 交互 → Presenter 构建路由模型 → Router 导航)
CellModel实现selectedCommand ,点击后将"选中模型"向上逐层传递 ,Presenter根据该模型构建NavigationModel (路由 ID + 参数 + 跳转方式),Router统一执行跳转与参数注入 ,Presenter层和View层不直接进行页面的跳转。相关核心方法为buildNavigationModelFromCellModel:
8.7 性能与体验优化
在实际落地过程中,针对性能和体验做了几方面改进:
- 请求防重 :Presenter层和View层均有activeRequests判重,避免同页并发请求产生问题。
- SWR的缓存机制 :命中缓存先渲染旧数据,随后真实请求再更新数据,提升首屏体验。
- UI刷新节流 :对来自Interactor层的增量更新viewUpdatedDataSignal 做节流,在View层通过throttle(0.3) 降低TableView刷新频率,避免频繁reload。
- 分页逻辑统一处理 :通过TJPPaginationInfo抽象页码/游标两种分页,统一在Interactor层中维护保证分页信息一致性。
- 可观测性:贯穿请求、分页、缓存等环节的轻量日志,便于定位问题。
九、总结
我们从VIPER的概念与落地难点 出发,逐步展开:为什么需要面向协议的抽象与模板化设计,为什么要引入一些框架;随后讨论了依赖注入、响应式绑定、生命周期管理等核心要素,并在一个复杂页面案例中展示了VIPER在真实环境下如何协同工作。
需要强调的一点是:VIPER的价值,或者说架构演进的价值 ,不在于写出一个"看上去很清晰"的 Demo ,而在于当业务扩展、团队壮大、协作加深时,依然能保持可维护、可扩展、可测试 。这也是为什么我们强调从工程化角度去定义基类、抽象通用逻辑、规范生命周期。
当然,VIPER并不是唯一解,适合自己业务的才是最好的。依赖注入可以不用Typhoon,RAC可以替换为Combine,OC项目也能渐进式过渡到Swift------这些替换都不会改变核心思路:职责分离、状态驱动、协议抽象。
所以,VIPER并没有大家想象中那么可怕。它不是要成为框架的负担,而是要成为在复杂业务和团队协作下,支撑代码长期演进的基础。希望本文能为探索架构演进的开发者,提供一些可落地的参考与启发。同时,因为笔者水平有限,文章以及代码中难免有些纰漏,欢迎大家指正!
可能会有同学疑惑:为什么没有展开讲 Router?
是的,Router 涉及到比较复杂的抽象与设计,篇幅所限只能放在下一篇。
目前文中的案例源码已开源在仓库:开源仓库 下的VIPER-Demo 中。后续我会考虑将其从主仓库中拆出一个独立的VIPER架构仓库,并计划尝试实现一个Swift + Combine的版本(先画个饼,哈哈哈)。