实现一套轻量级MVVM框架

·  阅读 5174

2021-04-04

修订记录:2021-05-17:Demo Controller 的实现标准化,Controller 仅保留订阅操作;2021-05-18:补充 MVVM Demo 单元测试示例。

前言

在客户端开发项目中,MVC 仍然是主流架构,但是 MVC 也存在十分明显的弊端:Controller 作为中介者常常需要负担大量的业务处理逻辑,所以 MVC 也被戏称为 Masive View Controller 架构。缓解这个问题其实有很多途径,例如:

  • 用胖模型分担 Controller 的模型数据处理工作,提供尽量熟成的业务数据;
  • 引入 Manager 或提取 Service 模块,负责特定业务模块数据管理;
  • 将膨胀的 Controller 业务分摊到多个 Child View Controller;
  • 通过 Category 对 Controller 业务逻辑做分文件处理;

此外,MVC 架构模式还普遍存在单元测试推进困难的问题,该问题还是来源于 Controller 负担过重,由于 Controller 通常需要依赖 View 层和 Model 层模块,引入 Manager 和 Service 则依赖模块更加繁多,因此测试 Controller 时通常需要 Mock 很多模块,打很多的 Stub 以剔除依赖模块的影响。另外,Controller 的单元测试需要考虑 Controller 复杂的生命周期。

MVVM 可以说是为了解决 MVC 的以上两个弊端而存在的。

一、MVVM架构

View Model 是 MVVM 架构的核心逻辑层。View Model 用于表征 View 的特性,并通过数据双向绑定 View Model 和 View,使 View Model 可以驱动 View 刷新界面,同时 View 接收的用户交互动作也可以更新 View Model 的数据。双向绑定下的 View Model 和 View 是完全解耦的,因此单元测试工作比起 MVC 架构简单得多。MVVM 中 Controller 的职能只局限于 View Model 和 View 的双向绑定,Controller 逻辑层变得很薄,因此周期问题基本可以忽略不计。

MVVM 之所以能达到以上效果,很大程度上是因为它将 View 和 View Model 之间的双向数据流下沉到了框架层。不过这也给 MVVM 带来了最大的缺点:数据流动控制的实现细节隐藏于核心框架层。近乎黑盒的双向数据绑定实现,有时会给代码调试带来不小的麻烦。另外,随着核心逻辑大量转移到 View Model,同样会带来 View Model 规模膨胀的问题,另外需要注意,MVVM 通常不能减少代码量,MVC 仍然是最省代码的客户端架构。总之就是,MVVM 值得尝试,但也没有想象中神奇。

在强大的 MVVM 框架支持下是可以达到更省代码的效果的。

二、MVVM架构实现

iOS 客户端开发中应用最为广泛的 MVVM 框架应该是 Objective-C 语言实现的 RAC 框架和 Swift 语言实现的 RxSwift 框架。RAC 框架给人的第一感觉就是重,在现有的 MVC 架构项目中引入 RAC 框架基本是颠覆性的,需要学习响应式编程、函数式编程、链式编程,需要熟悉 RAC 对 UIKit 框架的封装,更遑论还有一堆基于 RAC 的衍生框架。RxSwift 则还好一点,因为 RxSwift 的实现本身非常契合 Swift 语言的特点,不过依然是有点重。总之,在传统 MVC 项目中启用 RAC 或 RxSwift,绝对不比引入新编程语言的 Flutter、RN 来得简单。

那么可不可以用常规的,更轻量的方式来实现 MVVM 框架层逻辑呢?通常第一个想到的就是观察者模式,恰好 Objective-C 有强大的 KVO 机制。各逻辑分工大致如下:

  • Model:模型层;
  • View:视图层;
  • View Model:表征视图特性;
  • Controller:通过 KVO 设置 View 观察 View Model 的各个属性,则 View Model 属性值变更会驱动 View 刷新。另外,将来自 View 的用户交互触发的 Action 或者回调消息转发到 View Model 中处理;

但是在实现时你会发现,KVO 是通过 Key Path 来配置的,这个 Key Path 有个很致命的弱点,它是字符串!这会有什么问题呢?想象一下,有一天你要重构代码,发现某个 View Model 属性名设置不太合理,你用 Refactor Rename 工具给这个属性重命名了,此时所有通过 KVO 绑定 View Model 的该属性的业务逻辑都会出问题,这个时候你只能再手动修改该属性的数据绑定代码中对应的 Key Path 字符串。另外,字符串终究是字符串,IDE 不能为字符串提供编译时检查以及提示,所以可以预见开发体验极差。而且我认为 KVO 比较适用于上层业务实现,如果将其下沉到框架层则很容易和上层业务逻辑发生冲突。最简单的例子,如果上层模块实现observeValueForKeyPath:时,没有调用[super observeValueForKeyPath:],那底层的数据双向绑定框架就直接被旁路了。

KVO 方案被 Out 了,还有没有更好的实现方案呢?这就是下面所要探讨的问题。

三、轻量级MVVM架构方案

首先要引入 Observable 和 Observer 的概念,注意这里的 Observable 和 Observer 和 RAC 和 RxSwift 中 Observable 和 Observer 并不一致,甚至有点相反的意味。

  • Observable:可被观察对象;
  • Observer:观察者,可以订阅 Observable,当 Observable 刷新数据时,会触发 Observer 刷新;

非常直观地,有了 Observable 和 Observer 就可以打通数据(View Model)驱动界面刷新的单向数据流。那么从界面的用户交互 Action 或委托回调到 View Model 的反向数据流呢?其实也是可以通过 Observable 和 Observer 来打通,因为 Action 的本质其实也是传递数据,只要将来自 View 的用户交互 Action 所传递的数据定义为 Observable,将 View Model 定义为 Observer 即可以打通反向数据流。总之:

  • View Model 在数据驱动界面刷新数据流中扮演 Observable 的角色,此时 View 扮演 Observer 的角色;
  • View Model 在用户交互驱动数据更新数据流中扮演 Observer,View 扮演 Observable;

虽然用的是观察者模式,但是这里不使用 Notification 和 KVO,而是采用最简单粗暴的方法,Observable 强持有所有订阅该 Observable 的 Observer,Observable 值更新时直接触发所有 Observer 所注册的操作逻辑。看到这里可能会有这样的疑问,这不就循环引用了么?其实并不是,因为 Observable 的数据粒度要比 View Model 和 View 低一个等级,也就是说扮演 Observable 角色是指持有若干个 Observable 成员,扮演 Observer 角色是指持有若干个 Observer 成员。这样一来,View Model 和 View 就不会存在循环引用的问题。

3.1 基本接口

接下来是设计接口。首先按照 Observable 和 Observer 的定义,将其分别定义为两套协议:

Observable协议定义了可被观察者的基本特征:

  • Observable对应一个值value(公开 API);
  • 可以通过调用addObserver:方法向Observable添加观察者(供Observer调用);
  • 可以通过调用removeObserver:方法从Observable移除观察者(供Observer调用);
@protocol Observer;
/// 可观察对象,value 成员更新 setter 会驱动注册的观察者刷新。注册观察者后,观察者被可观察对象强持有
@protocol Observable <NSObject>

/// 值
@property(strong, nonatomic, nullable) id value;

/// 添加观察者
-(void)addObserver:(id<Observer>)observer;

/// 移除观察者
-(void)removeObserver:(id<Observer>)observer;

@end
复制代码

Observer协议定义了观察者的基本特征:

  • 可以通过访问subscribe属性订阅Observable(公开 API);
  • 可以通过调用invoke:方法触发刷新(供Observable调用);
@protocol Observable;
/// 观察者
@protocol Observer <NSObject>

/// 订阅可观察对象
@property(copy, nonatomic, readonly) void (^subscribe)(id<Observable> observable);

/// 触发值刷新
-(void)invoke:(id)newValue;

@end
复制代码

基于两个协议再进一步定义两个具体类型分别实现这两套协议。可以发现公开 API 都通过属性提供,之所以设计为这种形式,是为了在开发过程中使用优雅的链式调用风格。

/// 可观察对象
@interface Observable : NSObject<Observable>

/// 构建
@property(copy, nonatomic, class, readonly) Observable *(^create)(id _Nullable defaultValue);

@end

/// 观察者所注册的操作
typedef void(^ObserverHandler)(id newValue);

/// 观察者
@interface Observer : NSObject <Observer>

/// 构建
@property(copy, nonatomic, class, readonly) Observer *(^create)(void);

/// 处理值刷新
@property(copy, nonatomic, readonly) Observer * (^handle)(ObserverHandler);

@end
复制代码

3.2 基本实现

实现代码也非常简单,四个字概括:简单粗暴。Observable只管理一个值,而且必须是id类型。需要注意,Observable是具有原子性的(不是属性atomic那种原子性),也就是说,该框架只能区分Observable的值“改变”或者“不改变”,不存在Observable的值“只改变了其中一部分属性”这种状态。

@interface Observable ()

@property(strong, nonatomic) NSMutableArray *observers;

@end

@implementation Observable

@synthesize value = _value;

-(void)setValue:(id)value{
    if(![self.value isEqual:value]){
        _value = value;
        for(id<Observer> observer in self.observers){
            [observer invoke:value];
        }
    }
}

static Observable *(^create)(id) = ^Observable *(id defaultValue){
    Observable *observable = [[Observable alloc] init];
    observable.value = defaultValue;
    return observable;
};

+(Observable *(^)(id))create{
    return create;
}


-(void)addObserver:(id<Observer>)observer{
    [self.observers addObject:observer];
}

-(void)removeObserver:(id<Observer>)observer{
    [self.observers removeObject:observer];
}

-(NSMutableArray *)observers{
    if(!_observers){
        _observers = [[NSMutableArray alloc] init];
    }
    return _observers;
}

@end
复制代码

Observer实现同样简单粗暴。观察者持有一个 Block,Observerinvoke:方法只是简单调用了该 Block。在Observer订阅Observable时需要指定该 Block 的实现。问题又来了,这不就有循环引用的风险了么?没错,就是有循环引用的风险。但是只需要在调用subscribe时,在 Block 实现中使用__weak__strong避免强引用self即可,就是基本的 Block 防止循环引用的套路。虽然套路简单,但是需要注意这条规则一定要遵循

@interface Observer ()

@property(copy, nonatomic) Observer * (^handle)(ObserverHandler);

@property(copy, nonatomic) ObserverHandler handler;

@end

@implementation Observer

@synthesize subscribe = _subscribe;

-(void (^)(id<Observable>))subscribe{
    if(!_subscribe){
        __weak typeof(self) weakSelf = self;
        _subscribe = ^(id<Observable> observable){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            [observable addObserver:strongSelf];
        };
    }
    return _subscribe;
}

-(void)invoke:(id)newValue{
    if(self.handler){
        self.handler(newValue);
    }
}

static Observer *(^create)(void) = ^Observer *(){
    Observer *observer = [[Observer alloc] init];
    return observer;
};

+(Observer *(^)(void))create{
    return create;
}

-(Observer * (^)(ObserverHandler))handle{
    if(!_handle){
        __weak typeof(self) weakSelf = self;
        _handle = ^Observer *(ObserverHandler handler){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.handler = handler;
            return strongSelf;
        };
    }
    return _handle;
}

@end
复制代码

3.3 能力扩展

基本实现框架有了,不过仅有ObservableObserver的话,貌似只能组织最简单的数据流拓扑,即从单个Observable分发到多个Observer,其实开发过程中还希望具备多个Observable合成单个Observable的能力。为此定义ObservableCombiner用于实现Observable合成。

ObservableCombiner继承Observable类型,可以通过调用其combine属性合成多个Observable,同时ObservableCombiner具备一定Observer的特征(但不是遵循Observer协议),其handleObserversubscribe是相同的原理,只是传参上,前者是NSArray表示所有合成Observable的值的数组。当合成的Observable值更新时,会触发handle所注册的 Block。

/// 合成可观察对象的触发策略
typedef NS_ENUM(NSUInteger, CombineStrategy) {
    /// 第一次值更新才刷新
    CombineStrategyFirst,
    /// 所有值更新才刷新
    CombineStrategyAll,
    /// 任何值更新均刷新
    CombineStrategyEvery,
};

/// 合成可观察对象处理值刷新
typedef _Nullable id (^CombinerHandler)(NSArray *newValues);

/// 合成多个可观察对象
@interface ObservableCombiner : Observable

/// 安全获取值
@property(copy, nonatomic, readonly, class) id _Nullable (^safeValue)(NSArray *, NSInteger);

/// 构建
@property(copy, nonatomic, readonly, class) ObservableCombiner *(^create)(CombineStrategy strategy);

/// 合并可观察对象
@property(copy, nonatomic, readonly) ObservableCombiner * (^combine)(id<Observable> observable);

/// 处理值刷新
@property(copy, nonatomic, readonly) ObservableCombiner * (^handle)(CombinerHandler);

@end
复制代码

实现代码直接贴在文章最后,这里不作详细介绍了。

后面再尝试扩展支持从多个Observable映射到多个Observable的能力。

四、轻量级MVVM框架Demo

基于该框架的开发同样需要以组织数据流的思想作为指导,框架提供的公开 API 基本是用于组织数据流,核心操作是构建(create)、订阅(subscribe)和处理(handle)。为便于描述将 View Model 驱动 View 刷新数据流简称为正向数据流,将来自 View 的界面交互动作驱动 View Model 更新数据流简称为反向数据流,代码逻辑分布基本如下:

  • View Model:
    • 构建正向数据流的 Observable;
    • 构建正向数据流的合成 Observable;
    • 处理正向数据流的合成 Observable;
    • 构建反向数据流的 Observer;
    • 处理反向数据流的 Observer;(交互驱动数据刷新)
  • View:
    • 构建反向数据流的 Observable;
    • 构建反向数据流的合成 Observable;
    • 处理反向数据流的合成 Observable;
    • 构建正向数据流的 Observer;
    • 处理正向数据流的 Observer(数据驱动视图刷新);
  • Controller:
    • 订阅正向数据流的 Observable;
    • 订阅反向数据流的 Observable;

接下来以登录业务来演示轻量级 MVVM 框架的使用。业务描述如下:

  • 用户名必须超过 6 个字节不能超过 24 个字节;
  • 密码必须超过 6 个字节不能超过 24 个字节;
  • 用户名和密码不合法时登录按钮不可点击;
  • 用户名和密码不合法时给出相应提示,优先显示用户名的错误提示;

首先定义 View Model:

@interface LoginViewModel : NSObject

//MARK: 数据驱动UI刷新
@property(strong, nonatomic) Observable *username;

@property(strong, nonatomic) Observable *password;

@property(strong, nonatomic) Observable *instruction;

@property(strong, nonatomic) ObservableCombiner *usernameValid;

@property(strong, nonatomic) ObservableCombiner *passwordValid;

@property(strong, nonatomic) ObservableCombiner *loginEnabled;

//MARK: 用户交互动作订阅
@property(strong, nonatomic) Observer *usernameDidChange;

@property(strong, nonatomic) Observer *passwordDidChange;

@property(strong, nonatomic) Observer *loginTouched;

@end
复制代码

其次定义 View:

@interface LoginView : UIView

@property(strong, nonatomic) UITextField *usernameTextField;

@property(strong, nonatomic) UITextField *passwordTextField;

@property(strong, nonatomic) UILabel *instructionLabel;

@property(strong, nonatomic) UIButton *loginButton;

//MARK: 数据驱动UI刷新
@property(strong, nonatomic) Observer *username;

@property(strong, nonatomic) Observer *password;

@property(strong, nonatomic) Observer *instruction;

@property(strong, nonatomic) Observer *usernameValid;

@property(strong, nonatomic) Observer *passwordValid;

@property(strong, nonatomic) Observer *loginEnabled;

//MARK: 用户交互动作订阅
@property(strong, nonatomic) Observable *usernameDidChange;

@property(strong, nonatomic) Observable *passwordDidChange;

@property(strong, nonatomic) Observable *loginButtonTouched;

@end
复制代码

4.1 数据流组织

正向数据流组织及反向数据流处理是 View Model 的核心逻辑。组织正向数据流的代码如下,通过代码可以非常直观地阅读出以下关键信息,从而生成非常清晰的正向数据流拓扑:

data-stream-topology.jpg

  • usernameValid依赖于usernamepasswordValid的值;
  • passwordValid依赖于passwordusernameValid的值;
  • loginEnabled依赖于usernameValidpasswordValid的值;
-(void)doDataWeaving{
    self.usernameValid
        .combine(self.username)
        .combine(self.passwordValid)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            NSString *username = ObservableCombiner.safeValue(newValues, 0);
            if(!username.length){
                self.instruction.value = @"*用户名不能为空";
                return @(NO);
            }
            
            if(username.length < 6){
                self.instruction.value = @"*用户名必须超过6个字符";
                return @(NO);
            }
            
            if(username.length > 24){
                self.instruction.value = @"*用户名不能超过24个字符";
                return @(NO);
            }
            
            BOOL passwordValid = [ObservableCombiner.safeValue(newValues, 1) boolValue];
            if(passwordValid){
                self.instruction.value = @"";
            }
            return @(YES);
        });
    
    self.passwordValid
        .combine(self.usernameValid)
        .combine(self.password)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            NSString *password = ObservableCombiner.safeValue(newValues, 1);
            BOOL usernameValid = [ObservableCombiner.safeValue(newValues, 0) boolValue];
            
            BOOL passwordValid;
            NSString *passwordInstruction;
            if(!password.length){
                passwordInstruction = @"*密码不能为空";
                passwordValid = NO;
            }else if(password.length < 6){
                passwordInstruction = @"*密码必须超过6个字符";
                passwordValid = NO;
            }else if(password.length > 24){
                passwordInstruction = @"*密码不能超过24个字符";
                passwordValid = NO;
            }else{
                passwordInstruction = @"";
                passwordValid = YES;
            }
            
            if(usernameValid){
                self.instruction.value = passwordInstruction;
            }
            return @(passwordValid);
        });
    
    self.loginEnabled
        .combine(self.usernameValid)
        .combine(self.passwordValid)
        .handle(^id _Nullable(NSArray * _Nonnull newValues) {
            BOOL usernameValid = [ObservableCombiner.safeValue(newValues, 0) boolValue];
            BOOL passworkValid = [ObservableCombiner.safeValue(newValues, 1) boolValue];
            return @(usernameValid && passworkValid);
        });
}
复制代码

处理反向数据流的代码如下,代码比较简单不作赘述:

-(void)doActionProcessing{
    WS
    // 用户交互处理
    self.usernameDidChange = Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.username.value = newValue;
    });
    self.passwordDidChange = Observer.create().handle(^(id  _Nonnull newValue) {
        SS
        self.password.value = newValue;
    });
    self.loginTouched = Observer.create().handle(^(id  _Nonnull newValue) {
        self.instruction.value = [NSString stringWithFormat:@"登录成功(*^▽^*)"];
    });
}
复制代码

4.2 订阅的实现

订阅是 Controller 绝对的核心逻辑,包括正向数据流和反向数据流订阅。正向数据流订阅的实现代码如下,包括:

  • 正向数据流 Observable 订阅;
  • 正向数据流 Observer 构建及处理;
/// 数据驱动UI刷新
-(void)doDataBindings{
    self.loginView.username.subscribe(self.loginViewModel.username);
    
    self.loginView.password.subscribe(self.loginViewModel.password);
    
    self.loginView.instruction.subscribe(self.loginViewModel.instruction);
    
    self.loginView.usernameValid.subscribe(self.loginViewModel.usernameValid);
    
    self.loginView.passwordValid.subscribe(self.loginViewModel.passwordValid);
    
    self.loginView.loginEnabled.subscribe(self.loginViewModel.loginEnabled);
}
复制代码

反向数据流订阅的实现代码如下;

/// 用户交互动作订阅
-(void)doActionBindings{
    self.loginViewModel.usernameDidChange.subscribe(self.loginView.usernameDidChange);
    
    self.loginViewModel.passwordDidChange.subscribe(self.loginView.passwordDidChange);
    
    self.loginViewModel.loginTouched.subscribe(self.loginView.loginButtonTouched);
}
复制代码

从代码上看,不难发现,该框架是一点都不省代码,同等的 MVC 架构实现比上面的实现在代码空间行数上少 50% 左右,但是基于该框架实现的业务代码数据流非常清晰,代码逻辑分布更加均匀,View Model 处理纯粹的业务逻辑,也非常契合引入 Manager 和 Service 模块分流数据管理负担的优化方式。

4.3 单元测试

使用 MVVM 模式必须要吃到可测试性提升的红利,否则 MVVM 框架的存在意义会大打折扣。从 Demo 的实现代码看出,LoginViewModel 和 LoginView 是完全解耦的,两者都只依赖于框架层的 Observer 和 Observable 接口,都具有非常高的独立性,和单元测试的 Isolated 原则非常契合。对 LoginViewModel 和 LoginView 的单元测试基本不需要借助 Mock 工具(Observer 和 Observable 很轻量,属于基本模型类型,不需要 Mock)。

对 LoginViewModel 的测试需要针对:

  • 当 LoginViewModel 的某个 Observable 属性的值发生变化时,可以触发订阅到该 Observable 属性的 Observable 的 handler Block,并传回正确的值;
  • 当 LoginViewModel 的某个 Observer 属性所订阅的 Observable 值发生变化时,可以触发该 Observer 的 handler Block,并传回正确的值;
@interface LoginViewModelTests : XCTestCase

@property(strong, nonatomic) LoginViewModel *viewModel;

@end

@implementation LoginViewModelTests

- (void)setUp {
    self.viewModel = [[LoginViewModel alloc] init];
}

//MARK: 测试Observable示例
- (void)testUsernameValid_usernameCantBeShorterThan6 {
    NSString *shortName = @"Tsu";
    
    Observer.create().handle(^(id  _Nonnull newValue) {
        XCTAssertFalse([newValue boolValue]);
    }).subscribe(self.viewModel.usernameValid);
    
    self.viewModel.username.value = shortName;
}

//MARK: 测试Observer示例
- (void)testUsernameDidChange_usernameChangeSynchronously {
    Observable *action = Observable.create(nil);
    self.viewModel.usernameDidChange.subscribe(action);
    
    action.value = @"Maruko";
    
    XCTAssertTrue([@"Maruko" isEqualToString:self.viewModel.username.value]);
}

...

@end
复制代码

对 LoginView 的单元测试基本同理,重点测试 Observable 和 Observer 的数据流可通。简单示例如下:

- (void)testUsernameValid_whenUsernameInvalid_backgroundLightRed {
    Observable *usernameValid = Observable.create(nil);
    self.loginView.usernameValid.subscribe(usernameValid);
    
    usernameValid.value = @(NO);
    
    CGFloat lightRedR, lightRedG, lightRedB, lightRedA;
    [LightRed getRed:&lightRedR green:&lightRedG blue:&lightRedB alpha:&lightRedA];
    CGFloat bgR, bgG, bgB, bgA;
    [self.loginView.usernameTextField.backgroundColor getRed:&bgR green:&bgG blue:&bgB alpha:&bgA];
    XCTAssertEqual(lightRedR, bgR);
    XCTAssertEqual(lightRedG, bgG);
    XCTAssertEqual(lightRedB, bgB);
    XCTAssertEqual(lightRedA, bgA);
}
复制代码

对 LoginViewController 的测试也是比较简单,不过需要借助 OCMock 之类的 Stub 工具,Demo 中暂时不补充这块内容。LoginViewController 单元测试需要通过 Stub 工具,确保 LoginViewController 在到达某个生命周期阶段时(例如 viewDidLoad),必要的构建和订阅过程有执行到位。具体如下:

  • LoginView 的构建;
  • LoginView 的 Observer 属性完成正确的订阅操作;
  • LoginViewModel 的构建;
  • LoginViewModel 的 Observer 属性完成正确的订阅操作;

五、总结

虽然只定义了ObservableObserverObservableCombiner,但是它已经具备了构建 MVVM 架构的基本能力了。首先,肉眼可见的,它足够轻量。其次,不存在前文所述 KVO 方案的缺陷。最后,它麻雀虽小,其实五脏俱全,正确使用该框架可以获得漂亮工整的代码逻辑结构。在后面 Demo 开发过程中实际应用该框架时,感觉开发体验总体还是不错的。

当然本方案还是有非常明显的缺陷的,例如:

  • Observable值类型只支持id类型;
  • 组织数据流拓扑结构支持还存在不小的缺失;
  • 存在冗余的数据刷新次数;
  • 实现代码增幅十分明显,本文 Demo 相对 MVC 架构同等实现,代码行数增加了 50%(空间行数);
  • 调试过程中数据流向跟踪困难;
  • 订阅时需要避免 Block 循环引用问题;

总之,本文的轻量级 MVVM 框架方案可以用来体验 iOS 客户端开发中的 MVVM 架构模式的应用,或者理解 MVVM 架构的原理。本方案优点和缺点同等明显,由于目前缺乏完备的测试,以及对复杂业务场景的实践案例支撑,暂时不打算直接应用到开发项目中

附录

附录一:Combiner实现

// ObservableCombiner.m

@interface ObservableCombiner ()

@property(strong, nonatomic) NSMutableArray *observables;

@property(assign, nonatomic) CombineStrategy strategy;

@property(assign, nonatomic) NSUInteger accessFlags;

@property(copy, nonatomic) ObservableCombiner * (^combine)(id<Observable> observable);

@property(copy, nonatomic) ObservableCombiner * (^handle)(CombinerHandler);

@property(copy, nonatomic) CombinerHandler handler;

@end

@implementation ObservableCombiner

static ObservableCombiner *(^create)(CombineStrategy strategy) = ^ObservableCombiner *(CombineStrategy strategy){
    ObservableCombiner *combiner = [[ObservableCombiner alloc] init];
    combiner.strategy = strategy;
    return combiner;
};

+(ObservableCombiner *(^)(CombineStrategy))create{
    return create;
}

-(ObservableCombiner * (^)(id<Observable>))combine{
    if(!_combine){
        __weak typeof(self) weakSelf = self;
        _combine = ^ObservableCombiner * (id<Observable> observable){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            
            NSInteger index = strongSelf.observables.count;
            id<Observer> observer = Observer.create().handle(^(id  _Nonnull newValue) {
                __strong typeof(weakSelf) strongSelf = weakSelf;
                [strongSelf handleNewValue:newValue index:index];
            });
            
            [observable addObserver:observer];
            [strongSelf.observables addObject:observable];
            return strongSelf;
        };
    }
    return _combine;
}

-(ObservableCombiner * (^)(CombinerHandler))handle{
    if(!_handle){
        __weak typeof(self) weakSelf = self;
        _handle = ^ObservableCombiner *(CombinerHandler handler){
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.handler = handler;
            return strongSelf;
        };
    }
    return _handle;
}

-(NSMutableArray *)observables{
    if(!_observables){
        _observables = [[NSMutableArray alloc] init];
    }
    return _observables;
}

-(void)handleNewValue:(id)newValue index:(NSInteger)index{
    // 根据不同的策略触发完成事件
    BOOL isFirst = !self.accessFlags;
    self.accessFlags |= (1 << index);
    switch (self.strategy) {
        case CombineStrategyFirst:{
            if(isFirst){
                self.value = self.handler([self getAllValues]);
            }
        }break;
        
        case CombineStrategyEvery:{
            self.value = self.handler([self getAllValues]);
        }break;
        
        case CombineStrategyAll:{
            NSUInteger allFlags = pow(2, self.observables.count) - 1;
            BOOL isAll = (allFlags & self.accessFlags) == allFlags;
            if(isAll){
                self.value = self.handler([self getAllValues]);
            }
        }break;
            
        default:
            break;
    }
}

-(NSArray *)getAllValues{
    NSMutableArray *result = [[NSMutableArray alloc] init];
    for(id<Observable> observable in self.observables){
        [result addObject:observable.value ?: [NSNull null]];
    }
    return result;
}

static id _Nullable (^safeValue)(NSArray *, NSInteger) = ^id (NSArray *values, NSInteger index){
    return values[index] == [NSNull null] ? nil : values[index];
};

+(id  _Nullable (^)(NSArray * _Nonnull, NSInteger))safeValue{
    return safeValue;
}

@end
复制代码

附录二:源码

[1] 源码及Demo地址

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改