从Demo到生产:VIPER架构的生产级模块化方案

前言

在iOS开发中,随着项目规模的扩大,传统的MVC模式往往会导致控制器臃肿、业务逻辑耦合严重、测试困难。为了解决这些问题,通过引入ViewModel分担部分逻辑,从而减轻控制器的负担,这就是MVVM模式。

然而在业务快速迭代的场景下,ViewModel往往逐渐演变成"臃肿版本的VM"------逻辑依旧集中、扩展和维护成本依然偏高。

这时,源自Clean ArchitectureVIPER 就值得被重新考虑。它通过更细粒度的职责拆分(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:负责模块之间的跳转与依赖管理,提供导航逻辑。

通过这种拆分,VIPERUI、业务逻辑、数据、导航分离开,使得每一层职责更加清晰,也让单元测试变得更容易。

如图所示,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处理循环依赖主要为两个关键点:两阶段注入机制至少一侧用属性注入(类似于打破循环引用机制)。

两阶段注入机制

  1. 先把对象创建出来,但不填充依赖,将实例放进容器的"集合",为了防止重复创建。
  2. 根据injectProperty或injectParameter去解析并注入依赖。

至少一侧用属性注入

  1. 如果用构造函数注入,此时需要再构造函数里传入依赖。如果A构造函数依赖B,B构造函数依赖A,就会造成死锁问题。
  2. 正确做法是一边用构造函数,另一边用属性注入避免死锁问题发生。

实际开发过程中,个人建议非必要不使用构造函数传入依赖,统一使用属性注入,可以防止死锁问题。

五、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)各自拥有独立的生命周期。

正确管理它们的创建、初始化、工作与销毁阶段,是防止内存泄漏和状态混乱的关键。

标准生命周期流程
  1. 创建阶段
  • 由DI容器或工厂方法负责创建实例,保证每个组件的实例化与职责边界保持一致,避免随意在 View 内直接[alloc init]。
  1. 注入阶段
  • 建立组件间依赖关系,比如View注入Presenter;Presenter注入Interactor、Router。
  1. 初始化阶段
  • 执行必要的初始化逻辑,例如:Presenter初始化业务状态,Interactor配置缓存、分页信息。在此阶段,不建议发起耗时操作,以免阻塞首屏加载。
  1. 绑定阶段
  • 建立信号订阅和数据流:Presenter绑定Interactor的数据更新信号,View绑定Presenter的状态输出信号

  • 注意事项:此处极易产生循环引用,必须通过weak-strong dance或RACDisposable来管理。

  1. 工作阶段
  • 组件进入正常运行:View响应用户交互并转发给Presenter,Presenter调度,Interactor执行业务,Router负责页面跳转。
  • Entity 在此阶段作为纯数据载体被频繁读写。
  1. 清理阶段
  • 当View即将销毁或业务完成时,需要显式清理:Presenter解除所有信号绑定,释放Interactor;Interactor停止数据订阅、清空缓存、关闭网络请求;Router清理临时导航状态。
  1. 销毁阶段
  • 对象被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提示,并可一键重试)。

这种设计有两个好处:

  1. 提高可维护性:新增状态时只需要在状态机枚举中补齐,转换规则增加新状态的转换逻辑即可。
  2. 状态与渲染解耦: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更新的两条路径:

  1. 常规数据返回后的刷新:状态机进入Content状态,VC执行reloadDataWithSectionModels:
  2. 增量数据流下的局部更新: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的版本(先画个饼,哈哈哈)。

相关推荐
维基框架6 小时前
维基框架 (Wiki FW) v1.1.1 | 企业级微服务开发框架
java·架构
如此风景6 小时前
AppDelegate 详解
ios
小马哥编程8 小时前
【软考架构】SOA与微服务解疑
微服务·云原生·架构
神一样的老师9 小时前
面向 6G 网络的 LLM 赋能物联网:架构、挑战与解决方案
网络·物联网·架构
如此风景10 小时前
Swift基础学习文档
ios
如此风景10 小时前
Object-C基础学习文档
ios
mldong11 小时前
开源项目推荐 _ mldong-art-design:企业级管理系统快速开发框架
前端·vue.js·架构
百锦再12 小时前
SQLSugar 封装原理详解:从架构到核心模块的底层实现
sql·mysql·sqlserver·架构·core·sqlsugar·net