实现一套轻量级MVVM框架
在客户端开发项目中,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,Observer
的invoke:
方法只是简单调用了该 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 能力扩展
基本实现框架有了,不过仅有Observable
和Observer
的话,貌似只能组织最简单的数据流拓扑,即从单个Observable
分发到多个Observer
,其实开发过程中还希望具备多个Observable
合成单个Observable
的能力。为此定义ObservableCombiner
用于实现Observable
合成。
ObservableCombiner
继承Observable
类型,可以通过调用其combine
属性合成多个Observable
,同时ObservableCombiner
具备一定Observer
的特征(但不是遵循Observer
协议),其handle
和Observer
的subscribe
是相同的原理,只是传参上,前者是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; Controller: 订阅正向数据流的 Observable; 订阅反向数据流的 Observable; 构建正向数据流的 Observer; 处理正向数据流的 Observer(数据驱动视图刷新);
看起来很绕,实际上原则可以归结为四条:
在数据流来源构建 Observable; 在 Controller 中订阅 Observable; 在数据流终点处理 Observable; 在数据流终点构建 Observer;
注意:虽然在 Controller 中处理正向数据流,但是处理逻辑必须是非常简单的操作,基本是原子操作,符合操作可以通过在 View 层定义并实现接口,对外向 Controller 提供。其实最合理的布局是在 View 中构建和处理正向数据流 Observer,不过当 View Model 可以提供非常熟成的数据时,Controller 通过一两句代码就可以调起 View 刷新视图,则没有必要因此引入实现逻辑过于简单的新接口。
接下来以登录业务来演示轻量级 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;
@property(strong, nonatomic) Observable *usernameDidChange;
@property(strong, nonatomic) Observable *passwordDidChange;
@property(strong, nonatomic) Observable *loginButtonTouched;
@end
4.1 数据流组织
正向数据流组织及反向数据流处理是 View Model 的核心逻辑。组织正向数据流的代码如下,通过代码可以非常直观地阅读出以下关键信息,从而生成非常清晰的正向数据流拓扑:
usernameValid
依赖于username
和passwordValid
的值;passwordValid
依赖于password
和usernameValid
的值;loginEnabled
依赖于usernameValid
和passwordValid
的值;
-(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)doActionHandlings{
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{
WS
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.usernameTextField.text = newValue;
}).subscribe(self.loginViewModel.username);
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.passwordTextField.text = newValue;
}).subscribe(self.loginViewModel.password);
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.instructionLabel.text = newValue;
}).subscribe(self.loginViewModel.instruction);
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.usernameTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed;
}).subscribe(self.loginViewModel.usernameValid);
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.passwordTextField.backgroundColor = [newValue boolValue] ? [UIColor whiteColor] : LightRed;
}).subscribe(self.loginViewModel.passwordValid);
Observer.create().handle(^(id _Nonnull newValue) {
SS
self.loginView.loginButton.enabled = [newValue boolValue];
self.loginView.loginButton.backgroundColor = [newValue boolValue] ? ThemeColor : LightGray;
[self.loginView.loginButton setTitleColor:[newValue boolValue] ? [UIColor whiteColor] : [UIColor darkGrayColor] forState:UIControlStateNormal];
}).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 模块分流数据管理负担的优化方式。
五、总结
虽然只定义了Observable
、Observer
、ObservableCombiner
,但是它已经具备了构建 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地址: https://github.com/Luminixus/DataDrivenMVVM.git
- EOF -
看完本文有收获?请分享给更多人
关注「 iOS大全 」加星标,关注 iOS 动态
点赞和在看就是最大的支持❤️