轉(zhuǎn)載:http://ios.jobbole.com/83602/
最近工作比較忙,但還是出來更新博客了,今天給大家分享一些ReactiveCocoa以及MVVM的一些東西,干活還是比較足的。在之前發(fā)表過一篇博文,名字叫做《iOS開發(fā)之淺談MVVM的架構(gòu)設(shè)計(jì)與團(tuán)隊(duì)協(xié)作》,大體上講的就是使用Block回調(diào)的方式實(shí)現(xiàn)MVVM的。在寫上篇文章時(shí)也知道有ReactiveCocoa這個(gè)函數(shù)響應(yīng)式編程的框架,并且有許多人用它來更好的實(shí)現(xiàn)MVVM。所以在上篇博客發(fā)表后,有些同行給評(píng)論建議看一下ReactiveCocoa的東西,所以就系統(tǒng)的看了一下ReactiveCocoa的東西。不過有一點(diǎn)要說明的就是,不使用ReactiveCocoa是可以實(shí)現(xiàn)MVVM的,并非使用MVVM模式你就必須的使用ReactiveCocoa的東西,你可以使用KVO,Block,Delegate,Navigation等手段,而ReactiveCocoa更優(yōu)雅的實(shí)現(xiàn)了這個(gè)過程。ReactiveCocoa就是一個(gè)響應(yīng)式編程的框架,它會(huì)使MVVM每層之間交互起來更為方便,所以長和MVVM聯(lián)系在一起。
一.函數(shù)響應(yīng)式編程(Function Reactive Programming)
關(guān)于函數(shù)響應(yīng)式編程的東西,我想引用國外這個(gè)ReactiveCocoa教學(xué)視頻(視頻鏈接https://vimeo.com/65637501)中的一張PPT來簡單的說一下什么是函數(shù)響應(yīng)式編程。那就直接上圖,下圖是上方視頻鏈接的截圖,很形象的解釋了什么是函數(shù)響應(yīng)式編程。簡單的說下方c = a + b 定義好后,當(dāng)a的值變化后,c的值就會(huì)自動(dòng)變化。不過a的值變化時(shí)會(huì)產(chǎn)生一個(gè)信號(hào),這個(gè)信號(hào)會(huì)通知c根據(jù)a變化的值來變化自己的值。b的值變化同樣也影響c的值。下圖很好的表達(dá)了這個(gè)思想。在此就不做贅述了。
二. ReactiveCocoa簡介
先簡單的介紹一下什么是ReactiveCocoa框架,然后通過實(shí)例好好的去搞一搞這個(gè)框架,最后就是如何在項(xiàng)目中使用了。關(guān)于
ReactiveCocoa的理解一些博客(見本篇博客中的鏈接分享)中把ReactiveCocoa比喻成管道,ReactiveCocoa中的
Signal就是管道中的水流。使用ReactiveCocoa可以方便的在MVVM各層之間架起溝通的管道,便于每層之間的交互。現(xiàn)在在我們做的工程中
已經(jīng)在使用ReactiveCocoa框架了,用起來的感覺是非常爽的,好用!
可以說ReactiveCocoa中核心是信號(hào)量機(jī)制,Signal在ReactiveCocoa中發(fā)揮著強(qiáng)大的不可代替的作用,可謂是
ReactiveCocoa的靈魂所。Signal是可以攜帶一些對(duì)象和參數(shù)的,你可以獲取該對(duì)象并且可以對(duì)該信號(hào)量攜帶的值進(jìn)行map,
filter等常用操作,操作后的值會(huì)和該信號(hào)量進(jìn)行綁定。先簡單的這么一說,后邊的部分回詳細(xì)的介紹如何讓信號(hào)量發(fā)揮強(qiáng)大的作用。
ReactiveCocoa中對(duì)Block的使用可謂是淋漓盡致,如果對(duì)Block使用不熟的朋友可以補(bǔ)一下Block的東西,然后在回頭看一下ReactiveCocoa的東西。關(guān)于ReactiveCocoa更多的東西,請參考Github上的鏈接(https://github.com/ReactiveCocoa/ReactiveCocoa)。
三. 在工程中引入ReactiveCocoa
1.你可以使用Github上的加入方式如下所示,本人感覺比較麻煩,就沒有使用,采用的第二種方法(CocoaPods)。
2.上面的步驟難免有些麻煩,所以用CocoaPods更為便捷一些,Profile文件中的內(nèi)容如下所示,我用的是2.5版本。3.0后就支持
Swift了,因?yàn)槲覜]有用Swift寫東西,所以就用的是2.5版本,設(shè)置完P(guān)rofile文件后,pod install即可。
你可以pod search ReactiveCocoa看一下版本,選擇你需要的版本即可。
四.使用ReactiveCocoa
下方會(huì)通過一些簡單的實(shí)例來介紹一下信號(hào)量機(jī)制和一些常用的方法。
1.引入相應(yīng)的頭文件
在工程中引入下方的頭文件(建議在Pch文件中引入)就可以使用我們的ReactiveCocoa框架了
Objective-C
#import
#import
1
2#import
#import
2. Sequence和Map
Sequence:隊(duì)列,是ReactiveCocoa中引入的一個(gè)類型,它類似于數(shù)組,我們可以暫且把Sequence看做綁定信號(hào)
量的數(shù)組吧。在OC中的NSArray可以通過rac_sequence方法轉(zhuǎn)換成ReactiveCocoa中的Sequence,然后就可以調(diào)用處理
信號(hào)的一些方法了。
參考以下實(shí)例代碼:
(1)把NSArray通過rac_sequence方法生成RAC中的Sequence
(2)獲取該Sequence對(duì)象的信號(hào)量
(3)調(diào)用Signal的Map方法,使每個(gè)元素的首字母大寫
(4)通過subscribNext方法對(duì)其進(jìn)行遍歷輸出
Objective-C
//uppercaseString use map
- (void)uppercaseString {
RACSequence *sequence = [@[@"you", @"are", @"beautiful"] rac_sequence];
RACSignal *signal =? sequence.signal;
RACSignal *capitalizedSignal = [signal map:^id(NSString * value) {
return [value capitalizedString];
}];
[signal subscribeNext:^(NSString * x) {
NSLog(@"signal --- %@", x);
}];
[capitalizedSignal subscribeNext:^(NSString * x) {
NSLog(@"capitalizedSignal --- %@", x);
}];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//uppercaseString use map
-(void)uppercaseString{
RACSequence*sequence=[@[@"you",@"are",@"beautiful"]rac_sequence];
RACSignal*signal=sequence.signal;
RACSignal*capitalizedSignal=[signalmap:^id(NSString*value){
return[valuecapitalizedString];
}];
[signalsubscribeNext:^(NSString*x){
NSLog(@"signal --- %@",x);
}];
[capitalizedSignalsubscribeNext:^(NSString*x){
NSLog(@"capitalizedSignal --- %@",x);
}];
}
下方截圖是上個(gè)這個(gè)方法中的運(yùn)行結(jié)果,從運(yùn)行結(jié)果不難看出,通過Signal相應(yīng)的方法處理完后,處理的結(jié)果會(huì)與新返回的信號(hào)量所綁定。原信號(hào)量中的值保持不變。每次信號(hào)量調(diào)用相應(yīng)的方法處理完數(shù)據(jù)后,都會(huì)返回一個(gè)新的信號(hào)量,而這個(gè)信號(hào)量是獨(dú)立于原信號(hào)量的。
由上面的介紹可知,上面方法中的一坨代碼可以寫成下方的一串。因?yàn)橐粋€(gè)方法調(diào)用后會(huì)返回一個(gè)持有新結(jié)果的新的信號(hào)量,然后在這個(gè)信號(hào)量的基礎(chǔ)上再次
調(diào)用信號(hào)量其他的方法。Signal還有其他一些好用的方法,用法和map方法類似,在此就不一一贅述了,gitHub上有相應(yīng)的實(shí)例文檔。
Objective-C
- (void)uppercaseString {
[[[@[@"you", @"are", @"beautiful"] rac_sequence].signal
map:^id(NSString * value) {
return [value capitalizedString];
}] subscribeNext:^(id x) {
NSLog(@"capitalizedSignal --- %@", x);
}];
}
1
2
3
4
5
6
7
8
9-(void)uppercaseString{
[[[@[@"you",@"are",@"beautiful"]rac_sequence].signal
map:^id(NSString*value){
return[valuecapitalizedString];
}]subscribeNext:^(idx){
NSLog(@"capitalizedSignal --- %@",x);
}];
}
3.信號(hào)量開關(guān)(Switch)
上面把信號(hào)量比喻成水管,那么Switch就是水龍頭呢。通過Switch我們可以控制那個(gè)信號(hào)量起作用,并且可以在信號(hào)量之間進(jìn)行切換。也可以這
么理解,把Switch看成另一段水管,Switch對(duì)接那個(gè)水管,就流那個(gè)水管的水,這樣比喻應(yīng)該更為貼切一些。下方是一個(gè)關(guān)于Switch的一個(gè)小實(shí)
例。
(1) 首先創(chuàng)建3個(gè)自定義信號(hào)量(3個(gè)水管),前兩個(gè)水管是用來接通不同的水源的(google, baidu),
而最后一個(gè)信號(hào)量是用來對(duì)接不同水源水管的水管(signalOfSignal)。signalOfSignal接baidu水管上,他就流baidu水
源的水,接google水管上就流google水源的水。
(2) 把signalOfSignal信號(hào)量通過switchToLatest方法加工成開關(guān)信號(hào)量。
(3) 緊接著是對(duì)通過開關(guān)數(shù)據(jù)進(jìn)行處理。
(4) 開關(guān)對(duì)接baidu信號(hào)量,然后baidu和google信號(hào)量同時(shí)往水管里灌入數(shù)據(jù),那么起作用的是baidu信號(hào)量。
(5) 開關(guān)對(duì)接google信號(hào)量,google和baidu信號(hào)量發(fā)送數(shù)據(jù),則google信號(hào)量輸出到signalOfSignal中
Objective-C
//信號(hào)開關(guān)Switch
- (void)signalSwitch {
//創(chuàng)建3個(gè)自定義信號(hào)
RACSubject *google = [RACSubject subject];
RACSubject *baidu = [RACSubject subject];
RACSubject *signalOfSignal = [RACSubject subject];
//獲取開關(guān)信號(hào)
RACSignal *switchSignal = [signalOfSignal switchToLatest];
//對(duì)通過開關(guān)的信號(hào)量進(jìn)行操作
[[switchSignal? map:^id(NSString * value) {
return [@"https//www." stringByAppendingFormat:@"%@", value];
}] subscribeNext:^(NSString * x) {
NSLog(@"%@", x);
}];
//通過開關(guān)打開baidu
[signalOfSignal sendNext:baidu];
[baidu sendNext:@"baidu.com"];
[google sendNext:@"google.com"];
//通過開關(guān)打開google
[signalOfSignal sendNext:google];
[baidu sendNext:@"baidu.com/"];
[google sendNext:@"google.com/"];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//信號(hào)開關(guān)Switch
-(void)signalSwitch{
//創(chuàng)建3個(gè)自定義信號(hào)
RACSubject*google=[RACSubjectsubject];
RACSubject*baidu=[RACSubjectsubject];
RACSubject*signalOfSignal=[RACSubjectsubject];
//獲取開關(guān)信號(hào)
RACSignal*switchSignal=[signalOfSignalswitchToLatest];
//對(duì)通過開關(guān)的信號(hào)量進(jìn)行操作
[[switchSignalmap:^id(NSString*value){
return[@"https//www."stringByAppendingFormat:@"%@",value];
}]subscribeNext:^(NSString*x){
NSLog(@"%@",x);
}];
//通過開關(guān)打開baidu
[signalOfSignalsendNext:baidu];
[baidusendNext:@"baidu.com"];
[googlesendNext:@"google.com"];
//通過開關(guān)打開google
[signalOfSignalsendNext:google];
[baidusendNext:@"baidu.com/"];
[googlesendNext:@"google.com/"];
}
上面代碼輸出結(jié)果如下:
4.信號(hào)量的合并
信號(hào)量的合并說白了就是把兩個(gè)水管中的水合成一個(gè)水管中的水。但這個(gè)合并有個(gè)限制,當(dāng)兩個(gè)水管中都有水的時(shí)候才合并。如果一個(gè)水管中有水,另一個(gè)水
管中沒有水,那么有水的水管會(huì)等到無水的水管中來水了,在與這個(gè)水管中的水按規(guī)則進(jìn)行合并。下面這個(gè)實(shí)例就是把兩個(gè)信號(hào)量進(jìn)行合并。
(1) 首先創(chuàng)建兩個(gè)自定義的信號(hào)量letters和numbers
(2) 吧兩個(gè)信號(hào)量通過combineLatest函數(shù)進(jìn)行合并,combineLatest說明要合并信號(hào)量中最后發(fā)送的值
(3) reduce塊中是合并規(guī)則:把numbers中的值拼接到letters信號(hào)量中的值后邊。
(4) 經(jīng)過上面的步驟就是創(chuàng)建所需的相關(guān)信號(hào)量,也就是相當(dāng)于架好運(yùn)輸?shù)墓艿馈=又覀兙涂梢酝ㄟ^sendNext方法來往信號(hào)量中發(fā)送值了,也就是往管道中進(jìn)行灌水。
Objective-C
//組合信號(hào)
- (void)combiningLatest{
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
[[RACSignal
combineLatest:@[letters, numbers]
reduce:^(NSString *letter, NSString *number){
return [letter stringByAppendingString:number];
}]
subscribeNext:^(NSString * x) {
NSLog(@"%@", x);
}];
//B1 C1 C2
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//組合信號(hào)
-(void)combiningLatest{
RACSubject*letters=[RACSubjectsubject];
RACSubject*numbers=[RACSubjectsubject];
[[RACSignal
combineLatest:@[letters,numbers]
reduce:^(NSString*letter,NSString*number){
return[letterstringByAppendingString:number];
}]
subscribeNext:^(NSString*x){
NSLog(@"%@",x);
}];
//B1 C1 C2
[letterssendNext:@"A"];
[letterssendNext:@"B"];
[numberssendNext:@"1"];
[letterssendNext:@"C"];
[numberssendNext:@"2"];
}
上面示例的運(yùn)行輸出結(jié)果如下:
下面是自己畫的原理圖,思路應(yīng)該還算是清晰。
5.信號(hào)的合并(merge)
信號(hào)合并就理解起來就比較簡單了,merge信號(hào)量規(guī)則比較簡單,就是把多個(gè)信號(hào)量,放入數(shù)組中通過merge函數(shù)來合并數(shù)組中的所有信號(hào)量為一個(gè)。類比一下,合并后,無論哪個(gè)水管中有水都會(huì)在merge產(chǎn)生的水管中流出來的。下方是merge信號(hào)量的代碼:
(1) 創(chuàng)建三個(gè)自定義信號(hào)量, 用于merge
(2) 合并上面創(chuàng)建的3個(gè)信號(hào)量
(3) 往信號(hào)里灌入數(shù)據(jù)
Objective-C
//合并信號(hào)
- (void)merge {
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *chinese = [RACSubject subject];
[[RACSignal
merge:@[letters, numbers, chinese]]
subscribeNext:^(id x) {
NSLog(@"merge:%@", x);
}];
[letters sendNext:@"AAA"];
[numbers sendNext:@"666"];
[chinese sendNext:@"你好!"];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//合并信號(hào)
-(void)merge{
RACSubject*letters=[RACSubjectsubject];
RACSubject*numbers=[RACSubjectsubject];
RACSubject*chinese=[RACSubjectsubject];
[[RACSignal
merge:@[letters,numbers,chinese]]
subscribeNext:^(idx){
NSLog(@"merge:%@",x);
}];
[letterssendNext:@"AAA"];
[numberssendNext:@"666"];
[chinesesendNext:@"你好!"];
}
上面代碼運(yùn)行結(jié)果如下:
上面示例的原理圖如下:
五. 在MVVM中引入RactiveCocoa
學(xué)以致用,最后來個(gè)簡單的實(shí)例,來感受一下如何在MVVM中使用RactiveCocoa。當(dāng)然今天RAC的應(yīng)用是非常簡單的,但原理就是這樣的。
接下啦我們要使用RAC模擬一下登錄功能,當(dāng)然,網(wǎng)絡(luò)請求也是模擬的,這不是重點(diǎn)。重點(diǎn)在于如何在MVVM各層之間使用RAC的信號(hào)量來更方便的在各個(gè)層
之間進(jìn)行響應(yīng)式數(shù)據(jù)交互。下面這個(gè)實(shí)例的UI是非常簡單的,并且實(shí)現(xiàn)起來也是灰常簡單的,關(guān)鍵還是在于RAC的應(yīng)用。
1.搭建Demo所需UI,用戶界面非常簡單,公有兩個(gè)用戶界面,一個(gè)是登錄頁面(兩個(gè)輸入框,一個(gè)登錄按鈕),一個(gè)是登錄后跳轉(zhuǎn)的頁面(一個(gè)展示
用戶名和密碼的Label)。下方是使用Storyboard實(shí)現(xiàn)的用戶登錄頁面。實(shí)現(xiàn)完后,個(gè)兩個(gè)頁面各自關(guān)聯(lián)一個(gè)ViewContorller類。
2.下方是整個(gè)小Demo的工程目錄,因?yàn)槲覀兘裉斓闹攸c(diǎn)是如何在MVVM中使用RAC,
所以重點(diǎn)在于RAC的應(yīng)用,對(duì)于MVVM的分層就簡化一些。下方有VC層,在VC層中有兩個(gè)視圖控制器,一個(gè)是登錄使用的視圖控制器
(ViewContorller)另一個(gè)是登錄成功后的視圖控制器(LoginSuccessViewController)。而ViewModel中則
是負(fù)責(zé)登錄的ViewModel業(yè)務(wù)邏輯層,該層中負(fù)責(zé)數(shù)據(jù)驗(yàn)證,網(wǎng)絡(luò)請求,數(shù)據(jù)存儲(chǔ)等一些與UI無關(guān)的業(yè)務(wù)邏輯。
3.實(shí)現(xiàn)登錄的ViewModel層
因?yàn)閂iewModel層是獨(dú)立于UI層而存在的,所以可以在沒有UI的情況下我們就可以去實(shí)現(xiàn)相應(yīng)模塊的ViewModel層。這正好減少了個(gè)個(gè)層次間的耦合性,同時(shí)也提高了可測試性,總體上改善了可維護(hù)性。好廢話少說,接下來要實(shí)現(xiàn)登錄的ViewModel層。
(1) 登錄ViewModel層對(duì)應(yīng)的類的頭文件中的內(nèi)容如下所示(VCViewModel.h),
其實(shí)下方一些常用的信號(hào)量可以抽象出來放到ViewModel的父類中,這為了簡化Demo沒有做父類的抽象。下方就是VCViewModel中
interface定義的公有屬性和公有方法(Public)。userName和password(NSString類型)
用來綁定用戶輸入的用戶名和密碼。下方三個(gè)自定義信號(hào)量successObject, failureObject, errorObject
用來發(fā)送網(wǎng)絡(luò)請求的數(shù)據(jù)。successObject負(fù)責(zé)處理網(wǎng)絡(luò)請求成功且符合正常業(yè)務(wù)邏輯的事件,
failureObject負(fù)責(zé)網(wǎng)絡(luò)請求成功不符合正常業(yè)務(wù)邏輯的處理,errorObject負(fù)責(zé)網(wǎng)絡(luò)異常處理。
Objective-C
//
//? VCViewModel.h
//? ReactiveCocoaDemo
//
//? Created by Mr.LuDashi on 15/10/19.
//? Copyright ? 2015年 ZeluLi. All rights reserved.
//
#import
@interface VCViewModel : NSObject
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *password;
@property (nonatomic, strong) RACSubject *successObject;
@property (nonatomic, strong) RACSubject *failureObject;
@property (nonatomic, strong) RACSubject *errorObject;
- (id) buttonIsValid;
- (void)login;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//
//??VCViewModel.h
//??ReactiveCocoaDemo
//
//??Created by Mr.LuDashi on 15/10/19.
//??Copyright ? 2015年 ZeluLi. All rights reserved.
//
#import
@interfaceVCViewModel: NSObject
@property(nonatomic,strong)NSString*userName;
@property(nonatomic,strong)NSString*password;
@property(nonatomic,strong)RACSubject*successObject;
@property(nonatomic,strong)RACSubject*failureObject;
@property(nonatomic,strong)RACSubject*errorObject;
-(id)buttonIsValid;
-(void)login;
@end
上面可能說的有些抽象,結(jié)合項(xiàng)目中的實(shí)例來解釋一下什么時(shí)候發(fā)送successObject信號(hào)量,如何發(fā)送failureObject信號(hào)量,何時(shí)使用errorObject信號(hào)量。
以某些理財(cái)App中購買理財(cái)產(chǎn)品的業(yè)務(wù)流程為例。在用戶下單之前先去判斷用戶是否實(shí)名認(rèn)證以及綁定銀行卡,如果用戶已經(jīng)實(shí)名和綁定銀行卡就走正常支
付流程(用戶就是想去下單購買),VM就往VC發(fā)送successObject信號(hào),當(dāng)前VC就會(huì)根據(jù)信號(hào)量的指示跳轉(zhuǎn)到下單支付頁面。
但是如果用戶沒有實(shí)名或者綁卡,那么VM就給VC發(fā)送failureObject信號(hào),根據(jù)信號(hào)量中的參數(shù)來判斷是走實(shí)名認(rèn)證流程還是走綁定銀行卡流
程。
errorObject就比較簡單了,網(wǎng)絡(luò)異常,后臺(tái)服務(wù)器拋出的異常等不需要iOS這邊做業(yè)務(wù)邏輯處理的,就放在errorObject中負(fù)責(zé)錯(cuò)誤信息
的展示。
文字說完了,如果有些小伙伴還不太明白,那看下面這張?jiān)韴D吧。把三種信號(hào)量我們可以類比成十字路口的紅綠燈。successObject就是綠
燈,可以走正常流程。failureObject是黃燈,先等一下,完成該做的就可以走綠燈了。而errorObject就是一紅燈,報(bào)錯(cuò)異常,終止業(yè)務(wù)
流程并提升錯(cuò)誤信息。有圖有真相,到這兒如果還不理解我就沒招了。
在Public方法中- (id) buttonIsValid; 負(fù)責(zé)返回登錄按鈕是否可用的信號(hào)量。- (void)login;發(fā)起網(wǎng)絡(luò)請求,調(diào)用登錄網(wǎng)絡(luò)接口。
(2)代碼的具體實(shí)現(xiàn)如下(VCViewModel.m中的代碼),私有屬性如下。userNameSignal用來存儲(chǔ)用戶名的信號(hào)量,passwordSignal是用來存儲(chǔ)密碼的信號(hào)量。reqestData則是用來存儲(chǔ)返回?cái)?shù)據(jù)的。
Objective-C
@interface VCViewModel ()
@property (nonatomic, strong) RACSignal *userNameSignal;
@property (nonatomic, strong) RACSignal *passwordSignal;
@property (nonatomic, strong) NSArray *requestData;
@end
1
2
3
4
5@interfaceVCViewModel()
@property(nonatomic,strong)RACSignal*userNameSignal;
@property(nonatomic,strong)RACSignal*passwordSignal;
@property(nonatomic,strong)NSArray*requestData;
@end
(3)VCViewModel的初始化方法如下,負(fù)責(zé)初始化屬性。
Objective-C
- (instancetype)init
{
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
_userNameSignal = RACObserve(self, userName);
_passwordSignal = RACObserve(self, password);
_successObject = [RACSubject subject];
_failureObject = [RACSubject subject];
_errorObject = [RACSubject subject];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-(instancetype)init
{
self=[superinit];
if(self){
[selfinitialize];
}
returnself;
}
-(void)initialize{
_userNameSignal=RACObserve(self,userName);
_passwordSignal=RACObserve(self,password);
_successObject=[RACSubjectsubject];
_failureObject=[RACSubjectsubject];
_errorObject=[RACSubjectsubject];
}
(4) 發(fā)送登錄按鈕是否可用信號(hào)的方法如下,主要用到了信號(hào)量的合并。
Objective-C
//合并兩個(gè)輸入框信號(hào),并返回按鈕bool類型的值
- (id) buttonIsValid {
RACSignal *isValid = [RACSignal
combineLatest:@[_userNameSignal, _passwordSignal]
reduce:^id(NSString *userName, NSString *password){
return @(userName.length >= 3 & password.length >= 3);
}];
return isValid;
}
1
2
3
4
5
6
7
8
9
10
11//合并兩個(gè)輸入框信號(hào),并返回按鈕bool類型的值
-(id)buttonIsValid{
RACSignal*isValid=[RACSignal
combineLatest:@[_userNameSignal,_passwordSignal]
reduce:^id(NSString*userName,NSString*password){
return@(userName.length>=3&password.length>=3);
}];
returnisValid;
}
(5) 模擬網(wǎng)絡(luò)請求的發(fā)送,并發(fā)出網(wǎng)絡(luò)請求成功的信號(hào),具體代碼如下
Objective-C
- (void)login{
//網(wǎng)絡(luò)請求進(jìn)行登錄
_requestData = @[_userName, _password];
//成功發(fā)送成功的信號(hào)
[_successObject sendNext:_requestData];
//業(yè)務(wù)邏輯失敗和網(wǎng)絡(luò)請求失敗發(fā)送fail或者error信號(hào)并傳參
}
1
2
3
4
5
6
7
8
9
10
11-(void)login{
//網(wǎng)絡(luò)請求進(jìn)行登錄
_requestData=@[_userName,_password];
//成功發(fā)送成功的信號(hào)
[_successObjectsendNext:_requestData];
//業(yè)務(wù)邏輯失敗和網(wǎng)絡(luò)請求失敗發(fā)送fail或者error信號(hào)并傳參
}
4. 上面是VM的實(shí)現(xiàn),如果要進(jìn)行單元測試的話,就對(duì)相應(yīng)的VM類進(jìn)行初始化,調(diào)用相應(yīng)的函數(shù)進(jìn)行單元測試即可。接著就是看如何在相應(yīng)的VC模塊中使用VM。
(1) 在VC中實(shí)例化相應(yīng)的VM類,并綁定相應(yīng)的參數(shù)和實(shí)現(xiàn)接收不同信號(hào)的方法,具體代碼如下:
Objective-C
//關(guān)聯(lián)ViewModel
- (void)bindModel {
_viewModel = [[VCViewModel alloc] init];
RAC(self.viewModel, userName) = self.userNameTextField.rac_textSignal;
RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;
RAC(self.loginButton, enabled) = [_viewModel buttonIsValid];
@weakify(self);
//登錄成功要處理的方法
[self.viewModel.successObject subscribeNext:^(NSArray * x) {
@strongify(self);
LoginSuccessViewController *vc = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"LoginSuccessViewController"];
vc.userName = x[0];
vc.password = x[1];
[self presentViewController:vc animated:YES completion:^{
}];
}];
//fail
[self.viewModel.failureObject subscribeNext:^(id x) {
}];
//error
[self.viewModel.errorObject subscribeNext:^(id x) {
}];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//關(guān)聯(lián)ViewModel
-(void)bindModel{
_viewModel=[[VCViewModelalloc]init];
RAC(self.viewModel,userName)=self.userNameTextField.rac_textSignal;
RAC(self.viewModel,password)=self.passwordTextField.rac_textSignal;
RAC(self.loginButton,enabled)=[_viewModelbuttonIsValid];
@weakify(self);
//登錄成功要處理的方法
[self.viewModel.successObjectsubscribeNext:^(NSArray*x){
@strongify(self);
LoginSuccessViewController*vc=[[UIStoryboardstoryboardWithName:@"Main"bundle:[NSBundlemainBundle]]instantiateViewControllerWithIdentifier:@"LoginSuccessViewController"];
vc.userName=x[0];
vc.password=x[1];
[selfpresentViewController:vcanimated:YEScompletion:^{
}];
}];
//fail
[self.viewModel.failureObjectsubscribeNext:^(idx){
}];
//error
[self.viewModel.errorObjectsubscribeNext:^(idx){
}];
}
(2) 點(diǎn)擊登錄按鈕,調(diào)用VM中登錄相應(yīng)的網(wǎng)絡(luò)請求方法即可
Objective-C
- (void)onClick {
//按鈕點(diǎn)擊事件
[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
[_viewModel login];
}];
}
1
2
3
4
5
6
7-(void)onClick{
//按鈕點(diǎn)擊事件
[[self.loginButtonrac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(idx){
[_viewModellogin];
}];
}
到此為止,一個(gè)完整模擬登錄模塊的RAC下的MVVM就實(shí)現(xiàn)完畢。當(dāng)然上面的Demo是非常簡陋的,還有好多地方需要進(jìn)化。不過麻雀雖小,道理你懂得。主要是通過上面的Demo來感受一下RAC中的信號(hào)量機(jī)制以及應(yīng)用場景。
5.上面代碼寫完,我們就可以運(yùn)行看一下運(yùn)行效果了,下方是運(yùn)行后的效果,
上述工程GitHub分享鏈接:https://github.com/lizelu/MVVMWithReactiveCocoa
其他參考資料:
https://github.com/ReactiveCocoa/ReactiveViewModel
http://www.teehanlax.com/blog/model-view-viewmodel-for-ios/
http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
http://nshipster.cn/reactivecocoa/
http://limboy.me/ios/2013/06/19/frp-reactivecocoa.html
https://vimeo.com/65637501
http://southpeak.github.io/blog/2014/08/08/mvvmzhi-nan-yi-:flickrsou-suo-shi-li/
http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-%5B%3F%5D-:xin-hao/
http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-er-:twittersou-suo-shi-li/
ViewModel:
Kicking off network or database requests
Determining when information should be hidden or shown
Date and number formatting
Localization
ViewController:
Layout
Animations
Device rotation
View and window transitions
Presenting loaded UI