最近在學習ReactiveCocoa,驚異于響應式編程強大的能力之余,發現了一個有疑惑的點。
ReactiveCocoa的核心在于萬物皆信號,響應式編程簡單比喻就是多米諾骨牌,牽一發動全身。
網上找了些簡單demo,大多是登錄場景的小demo,輸出賬號校驗格式再提交什么的。基本代碼如下
ViewModel.h
@interface ViewModel : NSObject
@property (nonatomic, strong) NSString *account;
@property (nonatomic, strong) RACCommand *buttonCommand;
@end
ViewModel.m
- (RACCommand *)subscribeCommand {
if (!_subscribeCommand) {
@weakify(self);
_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.validSignal signalBlock:^RACSignal *(id input) {
**
提交賬號密碼
*/
return nil;
}];
}
return _subscribeCommand;
}
- (RACSignal *)accountValidSignal {
if (!_accountValidSignal) {
_accountValidSignal = [RACObserve(self, account) map:^id(NSString *string) {
return @(account.length > 5);
}];
}
return _accountValidSignal;
}
ViewController.m
@implementation ViewController
- (void)viewDidLoad {
**
省略
*/
}
- (void)bindViewModel {
RAC(self.viewModel, account) = self.account.rac_textSignal;
self.submitButton.rac_command = self.viewModel.buttonCommand;
}
**
省略其他部分代碼
*/
@end
很簡單的demo,響應用戶輸入TextField,ViewModel的account可以一并更新,并且實時更新button狀態。
好,看到這幾行,你是不是完全沒有疑問,一個很簡單的MVVM的小demo而已。
我昨天也是這么想的。
今天無意間看了幾行ReactiveCocoa的源碼。
RACCommand.h
@interface RACCommand : NSObject
**
省略
*/
/// A signal of whether this command is currently executing.
///
/// This will send YES whenever -execute: is invoked and the created signal has
/// not yet terminated. Once all executions have terminated, `executing` will
/// send NO.
///
/// This signal will send its current value upon subscription, and then all
/// future values on the main thread.
@property (nonatomic, strong, readonly) RACSignal *executing;
/// A signal of whether this command is able to execute.
///
/// This will send NO if:
///
/// - The command was created with an `enabledSignal`, and NO is sent upon that
/// signal, or
/// - `allowsConcurrentExecution` is NO and the command has started executing.
///
/// Once the above conditions are no longer met, the signal will send YES.
///
/// This signal will send its current value upon subscription, and then all
/// future values on the main thread.
@property (nonatomic, strong, readonly) RACSignal *enabled;
**
省略
*/
@end
發現ReactiveCocoa框架里定義的屬性,幾乎都定義為RACSignal類型,仔細想了下,確實這樣更符合ReactiveCocoa的核心思想,萬物皆信號。
看完這個之后,我決定把上面的demo改一改:
ViewModel.h
@interface ViewModel : NSObject
@property (nonatomic, strong) RACSignal<NSString *> *account;
@property (nonatomic, strong) RACCommand *command;
@end
ViewModel.m
@interface
@end
@implementation
- (RACCommand *)subscribeCommand {
if (!_subscribeCommand) {
@weakify(self);
_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.validSignal signalBlock:^RACSignal *(id input) {
**
提交賬號密碼
*/
return nil;
}];
}
return _subscribeCommand;
}
- (RACSignal *)accountValidSignal {
if (!_accountValidSignal) {
_accountValidSignal = [RACObserve(self, account) map:^id(NSString *string) {
return @(account.length > 5);
}];
}
return _accountValidSignal;
}
ViewController.m
@implementation ViewController
- (void)viewDidLoad {
**
省略
*/
}
- (void)bindViewModel {
self.vm.account = self.account.rac_textSignal;
self.submitButton.rac_command = self.viewModel.buttonCommand;
}
**
省略其他部分代碼
*/
@end
這樣看代碼是不是更簡潔,更直觀。
很多人會問,那ViewModel的account值怎么從外部set呢?
這就牽涉到整體框架的數據流轉問題了。
大家知道,數據的產生,基本上有兩個源頭,一個是網絡請求,一個是用戶的action,在整個APP的開發過程中,其實我們就是一直在這兩個源頭的中間,做數據轉換和處理工作。
- 將用戶的action傳遞給服務端,這里產生的數據,基本上都是在各種UI組件中輸入的,UIKit+ReactiveCocoa做了很強大的支持,幾乎所有輸入組件的value都已經定義了各種RACsignal輸出,我們根本不需要set,因為set這個操作應該是用戶做的,不應該是代碼做的,很完美!
- 將服務端返回的數據展示給用戶,這里產生的數據,基本上都是在網絡請求中獲取的,或者從本地的數據庫里拉的,以用的最多的AFNetworking為例,ReactiveCocoa也不忘為AFNetworking寫了一個category,這邊的問題也解決了。
那這么寫有沒有問題呢?
有。
MVVM相較于MVC來說,不僅是解決了Controller臃腫的問題,也解決了Controller無法測試的問題,ViewModel是可以測試的。
那么如果把ViewModel中的所有屬性轉化成RACSignal類型的話,測試就無法進行了,這在選型的時候是個很重要的問題。