MVVM
- MVC ---> Model View Controller ----> 復雜應用 ---> Massive View Controller(重量級視圖控制器) 。所以需要給給視圖控制器瘦身.
- MVVM 是 MVC 模式的一種演進,主要解決了ViewController過于臃腫帶來的不易維護和測試問題。
- view 是一個殼 ,不關心數據,所呈現的內容都由 ViewController來提供。但是又不是直接去溝通viewmodel,而是用viewController來做中間的協調者。
- viewController 管理view和viewModel,當VC初始化的時候通過viewModel中的方法去取數據。盡量不涉及業務邏輯,讓 ViewModel 去做這些事情.只是一個中間人,接收 View 的事件、調用 ViewModel 的方法、響應 ViewModel 的變化。
- 好處:可以讓Controller更加簡單和輕便,而且ViewModel相對獨立,也更加方便測試和重用
Controller
- 可以被看成View
- 處理布局、動畫、接收系統事件、展示UI
View-Model
- 代表應用程序UI狀態的特殊類型的模型
- 獲取原始數據, 運用邏輯, 并處理成 view (controller) 的展示數據.
- 不能包含 View,不然就跟 View 產生了耦合,不方便復用和測試。
- 它(通常通過屬性)暴露給視圖控制器需要知道的僅關于顯示視圖工作的信息(理想地你不會暴漏你的 data-model 對象). 它還負責對上游數據的修改(比如更新模型/數據庫, API POST 調用).
- 富有邏輯和轉換數據并將結果存到屬性的功能. 理想上相同的輸入(比如網絡服務響應)將會導出相同的輸出(屬性的值). 這意味著盡可能多地消除由”應用世界”剩余部分帶來的可能影響輸出的因素。
- ViewModel 之間可以有依賴。比如VC的VM和tableViewCell的VM。
- ViewModel 避免過于臃腫,不然維護起來也是個問題.
- 不對視圖控制器以任何形式直接起作用或直接通知其變化.
當 view-model 的共有屬性發生變化 更新視圖方法
-
用對應的 view-model 來初始化視圖控制器
UIViewController *controller =[[UIViewController alloc] initWithViewModel: ViewModel];
-
無法在初始化時將 view-model 傳入 比如在 storyboard segue 或 cell dequeuing 的情況下,這時需要在視圖(控制器)中暴露一個公有可寫的 view-model 屬性
UITableViewCell *cell =[self.tableView dequeueReusableCellWithIdentifier: @"cell" forIndexPath: indexPath]; // 給暴露出來的viewmodl賦值 cell.viewModel = self.viewModel. tweets[indexPath. row];
-
可以在鉤子程序調用前傳入 view-model, 比如 init 和 viewDidLoad, 我們可以從view-model 的屬性初始化所有 UI 元素的狀態.
//dontDoThis1.m - (id) initWithViewModel:(MYTwitterLookupViewModel *) viewModel { self = [super init]; if (!self) return nil; _viewModel = viewModel; return self; } - (void) viewDidLoad { [super viewDidLoad]; _goButton.enabled = viewModel.isUsernameValid; _goButton.alpha = viewModel.isUsernameValid ? 1 : 0.5; // etc }
- Delegate
- KVO
- Block
- ReactiveCocoa
MVVM還有一個很重要的概念是 data binding,view的呈現需要data,這個data就是由ViewModel提供的,將view的data與ViewModel的data綁定后,將來雙方的數據只要一方有變化,另一方就能收到。這里有Github 開源的一個ViewModel Base Class。
函數響應式式編程
- Functional Reactive Programming(以下簡稱FRP)是一種響應變化的編程范式。
- FRP提供了一種信號機制來實現這樣的效果,通過信號來記錄值的變化。信號可以被疊加、分割或合并。通過對信號的組合,就不需要去監聽某個值或事件。
- 在 iOS/Objective-C 中達到純函數水平是很棘手的(Swift 提供了一些附加的函數性, 這會讓我們更接近).
- 簡單函數為例子, 它們可能接受一兩個參數并輸出一個結果. 數據輸入, 數據輸出.這個函數可能是做一些數學運算或是將姓和名結合到一起. 無論應用的其他地方發生啥, 這個函數總是對相同的輸入產生相同的輸出. 這就是函數式方面
- view-model 仍然只是一個對象, 而不用維護一些狀態(否則它將不會是你視圖中非常好用的模型了. )但你仍該努力將盡可能多的邏輯移到無狀態的函數”值”中. 再重復一次, Swift在這方面比 Objective-C 更加可行.
- 單元測試方便,bug 數量隨著狀態數遞減,避免了視圖控制器的復雜性
ReactiveCocoa
- 在iOS平臺上對FRP的實現。
- 核心:信號,用RACSignal實現。
RACSignal
- RACSignal是數據流,可以被綁定和傳遞.
-
RACSignal
(信號)是RAC的構造單元,作用就是可以預先運用邏輯并構建處理(陳述性),而不是非要等到事情發生后才處理(命令式)。 -
RACSignal
可以把所有這些異步方法(代理、block、通知、KVO、target/action等)統一到一個接口下。[圖片上傳失敗...(image-6223bb-1510888578514)] - 訂閱后,當訂閱者發生變化,
RACSignal
會向訂閱者一次發送0或多個載有數值的“next”事件,后面跟著一個“complete”或“error”事件。
常見關鍵詞
- createSignal:創建信號
- subscriber 有了訂閱者,傳送給訂閱者
- filter:過濾,符合條件才向下執行。
- defer : 將代碼的創建推遲到信號被訂閱
- concat: 連接信號,第一個信號必須發送完成,第二個信號才會被激活
- map : 修改/映射,將信號內容轉換。把信號改成需要的類型。
- takeUtil : signalA takeUntil:signalB 當signalB激活之后,停止signalA 的訂閱
- combineLatest:reduce:多個信號合成成一個新的信號,只要其中一個有響應,這個新的信號就會得到這個響應。
- combine:疊加
-
chain:串聯
combineLatest:reduce
RAC(self.myButton, enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id(NSString *userName, NSString *password) {
//如果用戶名和密碼框的長度都超過6,提交按鈕就enable
return @(userName.length >= 6 && password.length >= 6);
}];
RACObserve(TARGET, KEYPATH)
- 參數:
- TARGET:要觀察的對象
- KEYPATH: 要觀察的那個對象某屬性的keypath
- RACObserve 會創建一個信號, 一旦它有了訂閱者, 它就立刻發送那個屬性的當前值, 并發送那個屬性在這之后的任何變化。
//監聽button點擊事件
[[_myButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *button){
NSLog(@"點擊了button");
}];
//監聽textView.text
[[_myTextFiled rac_textSignal]subscribeNext:^(NSString * _Nullable x) {
NSLog(@"x---%@",x);
}];
注意: RAC 只處理對象, 而不處理像 BOOL 這樣的原始值. 不過不用擔心, RAC 通常會幫你這些轉換.
- 宏 RAC(TARGET, ...)與 宏RACObserve(TARGET, KEYPATH) 相同
- RAC(TARGET, ...) 想要與即將到來的值綁定的對象和參數
轉換數據流
-
map 方法:返回的是一個對象。
//將viewModel的isUsernameValid與myButton的enadbled和backgroundColor綁定。 RACSignal *userNameIsValidSignal = RACObserve(self.viewModel, isUsernameValid); RAC(self.myButton,highlighted) = userNameIsValidSignal; RAC(self.myButton,backgroundColor) = [userNameIsValidSignal map:^id _Nullable(NSNumber* _Nullable value) { return value.boolValue ? [UIColor redColor] : [UIColor blueColor]; }];
RAC的大統一
在 RAC 的文檔和一些介紹 RAC 的 Keynote 資料里面我們可以看到說 RACSignal 可以來替代 Delegate、 Block Callbacks、Target Action、KVO、Notifications。因為它們本質上都是值的變化(Values over time)。
KVO
RAC可以用來監測屬性的改變,這點跟KVO很像,不過使用了block,而不是-observeValueForKeyPath:ofObject:change:context:
[RACAble(self.username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
使用起來是不是比KVO舒服多了。比KVO更加強大的是信號可以被鏈起來(chain)
// 只有當名字以'j'開頭,才會被記錄
[[RACAble(self.username)
filter:^(NSString *newName) {
return [newName hasPrefix:@"j"];
}]
subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
UI Event
RAC還為系統UI提供了很多category,來方便消息的創建和傳遞,比如按鈕被點擊或文本框有改動等等。如果有subscriber的話,會自動觸發。
比如:self.usernametextField.rac_textSignal
Network Request && Async work
這些可以通過自定義信號,也就是RACSubject(繼承自RACSignal,可以理解為自由度更高的signal)來搞定.
//比如一個異步網絡操作,可以返回一個subject,然后將這個subject綁定到一個subscriber或另一個信號
- (void)doTest
{
RACSubject *subject = [self doRequest];
[subject subscribeNext:^(NSString *value){
NSLog(@"value:%@", value);
}];
}
- (RACSubject *)doRequest
{
RACSubject *subject = [RACSubject subject];
// 模擬2秒后得到請求內容
// 只觸發1次
// 盡管subscribeNext什么也沒做,但如果沒有的話map是不會執行的
// subscribeNext就是定義了一個接收體
[[[[RACSignal interval:2] take:1] map:^id(id _){
// the value is from url request
NSString *value = @"content fetched from web";
[subject sendNext:value];
return nil;
}] subscribeNext:^(id _){}];
return subject;
}
Signal and Subscriber
signal 和 subscriber 是RAC中最核心的,signal如果比喻成插座的話,subscriber 就是 插頭。插座負責去取電,插座負責使用。插頭可以無限串聯(chain)。當插座signal 什么都不干的時候,也就是處于冷狀態,有了插頭插才會去獲取,這個時候就處于熱狀態。
-
signal 獲取到數據后,會調用subscriber的
sendNext,sendComplete,sendError
方法來傳送數據,如果沒有sendComplete和sendError,新的值就會通過sendNext一直傳輸。//使用KVO監聽了property的變化,只要username變化,block就會執行。NSURLCache的currentDiskUsage就不能被RACObserve。 [RACObserve(self, username) subscribeNext: ^(NSString *newName){ NSLog(@"newName:%@", newName); }];
signal 很靈活,被修改(map),過濾(filter),疊加(combine),串聯(chain),這有助于應對更加復雜的情況.
//把登錄按鈕enable和用戶名輸入框、密碼輸入框、loggingIn、self.loggedIn 綁定。如果reduce 輸出NO,則登錄按鈕不能點擊,反之則可以點擊。返回的值必須是object對象。
RAC(self.logInButton, enabled) = [RACSignal
combineLatest:@[
self.usernameTextField.rac_textSignal,
self.passwordTextField.rac_textSignal,
RACObserve(LoginManager.sharedManager, loggingIn),
RACObserve(self, loggedIn)
] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
}];
冷信號(Cold)和熱信號(Hot)
- createSignal 創建了信號但是沒有訂閱,所以此時這個信號處于冷狀態。
- 當創建的信號被訂閱后,狀態才會變成熱狀態.
Cocoa Categories
為了更加方便地使用RAC,RAC給Cocoa添加了很多category,與系 統集成地越緊密,使用起來自然也就越方便
-
UIView Categories
//點擊button后會發出信號signal,通過rac_command處理 @weakify(self) self.myButton.rac_command = [[RACCommand alloc] initWithEnabled:self.enable signalBlock:^RACSignal *(id input) { // Assume that we're logged in at first. We'll replace this signal later if not. RACSignal *authSignal = [RACSignal empty]; //判斷條件是否通過 if (self.enable) { //未通過后的處理 return nil; }; return [authSignal then:^RACSignal *{ @strongify(self); //通過后的處理 return nil; }]; }]; [self.myButton.rac_command.errors subscribeNext:^(id x) { [x subscribeNext:^(NSError *error) { //失敗處理 }]; }];
-
常用的數據結構也添加了相應的category。
//NSArray為例 NSArray *array = @[@1,@12,@3,@4]; //有序的不可變集合,是運行時計算的,所以對性能提升有一定的幫助 RACSequence *sequece = [array rac_sequence]; //轉換成NSArray NSArray *testArray = [sequece array]; [sequece any:^BOOL(id _Nullable value) { //檢查sequece中的每一個元素是否符合要求 }]; [sequece all:^BOOL(id _Nullable value) { //檢查是不是所有的元素都符合要求 }];
-
通知(NotificationCenter)
默認情況下使用 通知 的話需要使用Target-Action方式來處理Notifiction,這樣就需要從新定義一個方法,而且還要起名字(編程領域的兩大難題之一)防止重名。還需要在合適的時候移除observer。//RAC 實現 不用擔心observer移除問題。 [[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) { NSLog(@"Notification Received"); }];
-
NSObject
-
一個object的dealloc被觸發時,執行的一段代碼.《NSObject+RACDeallocating.h》
```
NSArray *array = @[@"foo"];
[[array rac_willDeallocSignal] subscribeCompleted:^{
NSLog(@"oops, i will be gone");
}];
array = nil; -
滿足一定條件時,自動觸發某個方法《NSObject+RACLifting.h》
//當signalA和signalB都至少sendNext過一次,接下來只要其中任意一個signal有了新的內容,doA:withB這個方法就會自動被觸發。 - (void)test { RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { double delayInSeconds = 2.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ [subscriber sendNext:@"A"]; }); return nil; }]; RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [subscriber sendNext:@"B"]; [subscriber sendNext:@"Another B"]; [subscriber sendCompleted]; return nil; }]; [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil]; } - (void)doA:(NSString *)A withB:(NSString *)B { NSLog(@"A:%@ and B:%@", A, B); // A:A and B:Another B }
-
-
NSObject+RACSelectorSignal.h
- rac_signalForSelector:某個事件響應后同時做一個操作。相當于hook.
- rac_signalForSelector:fromProtocol:selector實現了某個協議,所以可以用它來實現Delegate
[[self rac_signalForSelector:@selector(test)]subscribeNext:^(RACTuple * _Nullable x) { //執行test方法的時候,也把這里執行了。 }]; //這個方法主要是把 protocal 轉為一個 Signal 便于使用。值得注意的是這個函數返回的是一個 RACTuple。 這個 RACTuple 包含了 Selector 方法里面所有的參數。這樣需要用到的時候主要按照順序來獲取。RAC 來替代 Delegate . [[self rac_signalForSelector:@selector(test) fromProtocol:(@protocol(UITableViewDelegate))]mapReplace:@YES];
小結:
- 當一個
signal
被subscriber
訂閱后,這個subscriber
會在執行到sendComplete
和sendError
移除。也可以手動移除[disposable dispose]
.當subscriber
被dispose
后,所有相關subscriber
相關的工作都會停止,資源也會釋放。 -
Signal evente
是線性的,不會出現并發的情況,除非顯示的指定Scheduler
,所以,-subscribeNext:error:completed:
里的block 不需要鎖定或者synchronized
等操作,其他的events
會依次排隊,直到block處理完成。 - Error有優先權,如果多個signal被同時監聽,只要有一個
signal SendError
就會被傳送給subscriber,并導致signal全部終止執行,相當于 Exception - 生成Signal時,最好指定Name,
-setNameWithFormat:
方便調試。 - block代碼中不要阻塞。
參考: