iOS響應式編程框架ReactiveCocoa講解與實例演示

Signal and Subscriber

     這是RAC最核心的內容,若是用插頭和插座來描述,插座是Signal,插頭是Subscriber。插座負責去獲取電,插頭負責使用電,並且一個插座能夠插任意數量的插頭。當一個插座(Signal)沒有插頭 (Subscriber)時什麼也不幹,也就是處於冷(Cold)的狀態,只有插了插頭時纔會去獲取,這個時候就處於熱(Hot)的狀態。react

     Signal獲取到數據後,會調用Subscriber的sendNext, sendComplete, sendError方法來傳送數據給Subscriber,Subscriber天然也有方法來獲取傳過來的數據,如:[signal subscribeNext:error:completed]。這樣只要沒有sendComplete和sendError,新的值就會經過 sendNext源源不斷地傳送過來。git

    RACObserve使用了KVO來監聽property的變化,只要username被本身或外部改變,block就會被執行。但不是全部的property均可以被RACObserve,該property必須支持KVO,好比NSURLCache的currentDiskUsage就不能被RACObservegithub

      Signal是很靈活的,它能夠被修改(map),過濾(filter),疊加(combine),串聯(chain),這有助於應對更加複雜的狀況,好比:編程

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);
        }];

         左邊的RAC(...),它的做用是將self.logInButton.enabled屬性與右邊的signal的sendNext值綁定。也就是若是右邊的reduce的返回值爲NO,那麼enabled就爲NO。右邊的combineLatest是獲取這4個signal的next值。其中能夠看到self.usernameTextField.rac_textSignal這麼個東東,rac_textSignal是 RAC爲UITextField添加的category,只要usernameTextField的值有變化,這個值就會被返回(sendNext)。 combineLatest須要每一個signal至少都有過一次sendNext。reduce的做用是根據接收到的值,再返回一個新的值,這裏是 @(YES)和@(NO),必須是object。vim

冷信號(Cold)和熱信號(Hot)

RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    NSLog(@"triggered");
    [subscriber sendNext:@"foobar"];
    [subscriber sendCompleted];
    return nil;
}];

建立了一個Signal,但由於沒有被subscribe,因此什麼也不會發生。因此此時處於冷信號狀態api

[signal subscribeCompleted:^{
    NSLog(@"subscription %u", subscriptions);
}];

加了上面這段代碼後,signal就處於Hot的狀態了,block裏的代碼就會被執行。緩存

或許有人會問,若是這時又有一個新的subscriber了,signal的block還會被執行嗎?這就牽扯到了另外一個概念:Side Effect數據結構

Side Effect

    仍是上面那段代碼,若是有多個subscriber,那麼signal就會又一次被觸發,控制檯裏會輸出兩次triggered。這或許是你想要的,或許不是。若是要避免這種狀況的發生,可使用[ signal replay ]方法,它的做用是保證signal只被觸發一次,而後把sendNext的value存起來,下次再有新的subscriber時,直接發送緩存的數據。架構

Cocoa Categories

    爲了更加方便地使用RAC,RAC給Cocoa添加了不少category,與系統集成地越緊密,使用起來天然也就越方便。下面是我認爲比較經常使用的categories。併發

  UIView Categories

上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其餘經常使用的UIView也都有添加相應的category,好比UIAlertView,就不須要再用Delegate了。

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil];
[[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {
    if ([indexNumber intValue] == 1) {
        NSLog(@"you touched NO");
    } else {
        NSLog(@"you touched YES");
    }
}];
[alertView show];

    有了這些Category,大部分的Delegate均可以使用RAC來作。或許你會想,可不能夠subscribe NSMutableArray.rac_sequence.signal,這樣每次有新的object或舊的object被移除時都能知 道,UITableViewController就能夠根據dataSource的變化,來reloadData。但很惋惜這樣不行,由於RAC是基於 KVO的,而NSMutableArray並不會在調用addObject或removeObject時發送通知,因此不可行。不過可使用 NSArray做爲UITableView的dataSource,只要dataSource有變更就換成新的Array,這樣就能夠了。

    說到UITableView,再說一下UITableViewCell,RAC給UITableViewCell提供了一個方法:rac_prepareForReuseSignal,它的做用是當Cell即將要被重用時,告訴Cell。想象Cell上有多個button,Cell在初始化時給每一個button都addTarget:action:forControlEvents,被重用時須要先移除這些target,下面這段代碼就能夠很方便地解決這個問題:

[[[self.cancelButton rac_signalForControlEvents:UIControlEventTouchUpInside]
    takeUntil:self.rac_prepareForReuseSignal]
    subscribeNext:^(UIButton *x) {
    // do other things
}];

還有一個很經常使用的category就是UIButton+RACCommandSupport.h,它提供了一個property:rac_command,就是當button被按下時會執行的一個命令,命令被執行完後能夠返回一個signal,有了signal就有了靈活性。好比點擊投票按鈕,先判斷一下有沒有登陸,若是有就發HTTP請求,沒有就彈出登錄框,能夠這麼實現。

 logInButton.rac_command = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
        return [RACSignal empty];
    }];
//下面的代碼摘自AshFurrow的FunctionalReactivePixels。
voteButton.rac_command = [[RACCommand alloc] initWithEnabled:self.viewModel.voteCommand.enabled 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 ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) { // Not logged in. Replace signal. authSignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { @strongify(self); FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName:@"FRPLoginViewController" bundle:nil]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; [self presentViewController:navigationController animated:YES completion:^{ [subscriber sendCompleted]; }]; return nil; }]]; } return [authSignal then:^RACSignal *{ @strongify(self); return [[self.viewModel.voteCommand execute:nil] ignoreValues]; }]; }]; [voteButton.rac_command.errors subscribeNext:^(id x) { [x subscribeNext:^(NSError *error) { [SVProgressHUD showErrorWithStatus:[error localizedDescription]]; }]; }];

Data Structure Categories

    經常使用的數據結構,如NSArray, NSDictionary也都有添加相應的category,好比NSArray添加了rac_sequence,能夠將NSArray轉換爲RACSequence,順便說一下RACSequence, RACSequence是一組immutable且有序的values,不過這些values是運行時計算的,因此對性能提高有必定的幫助。RACSequence提供了一些方法,如array轉換爲NSArrayany:檢查是否有Value符合要求,all:檢查是否是全部的value都符合要求,這裏的符合要求的,block返回YES,不符合要求的就返回NO。

NotificationCenter Category

   NSNotificationCenter, 默認狀況下NSNotificationCenter使用Target-Action方式來處理Notification,這樣就須要另外定義一個方法,這就涉及到編程領域的兩大難題之一:起名字。有了RAC,就有Signal,有了Signal就能夠subscribe,因而NotificationCenter就能夠這麼來處理,還不用擔憂移除observer的問題。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) {
    NSLog(@"Notification Received");
}];

NSObject Categories

NSObject有很多的Category,我以爲比較有用的有這麼幾個

NSObject+RACDeallocating.h

顧名思義就是在一個object的dealloc被觸發時,執行的一段代碼。

NSArray *array = @[@"foo"];
[[array rac_willDeallocSignal] subscribeCompleted:^{
    NSLog(@"oops, i will be gone");
}];
array = nil;

NSObject+RACLifting.h

有時咱們但願知足必定條件時,自動觸發某個方法,有了這個category就能夠這麼辦

- (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);
}

這裏的rac_liftSelector:withSignals 就是幹這件事的,它的意思是當signalA和signalB都至少sendNext過一次,接下來只要其中任意一個signal有了新的內容,doA:withB這個方法就會自動被觸發。

若是你有興趣,能夠想一想上面這段代碼會輸出什麼。

NSObject+RACSelectorSignal.h

這個category有rac_signalForSelector:rac_signalForSelector:fromProtocol: 這兩個方法。先來看前一個,它的意思是當某個selector被調用時,再執行一段指定的代碼,至關於hook。好比點擊某個按鈕後,記個日誌。後者表示該selector實現了某個協議,因此能夠用它來實現Delegate。

MVVM

RAC帶來的變化還不只僅是這些,它還帶來了架構層面的變化。咱們都知道蘋果推薦的是MVC架構,那MVVM又是什麼呢?

跟MVC最大的區別是多了個ViewModel,它直接與View綁定,並且對View一無所知。拿作菜打比方的話,ViewModel就是調料,它不關心作的究竟是什麼菜。這不是跟Model很像嗎?是的,它能夠扮演Model的職責,但其實它是Model的中介,這樣當Model的API有變化,或者由本地存儲變爲遠程API調用時,ViewModel的public API能夠保持不變。

使用ViewModel的好處是,可讓Controller更加簡單和輕便,並且ViewModel相對獨立,也更加方便測試和重用。那 Controller這時又該作哪些事呢?在MVVM體系中,Controller能夠被當作View,因此它的主要工做是處理佈局、動畫、接收系統事 件、展現UI。

MVVM還有一個很重要的概念是 data binding,view的呈現須要data,這個data就是由ViewModel提供的,將view的data與ViewModel的data綁定後,未來雙方的數據只要一方有變化,另外一方就能收到。這裏有Github 開源的一個ViewModel Base Class。

其餘

RAC在使用時有一些注意事項,能夠參考官方的DesignGuildLines,這裏簡單說一下。

當一個signal被一個subscriber subscribe後,這個subscriber什麼時候會被移除?答案是當subscriber被sendComplete或sendError時,或者手動調用[disposable dispose]。

當subscriber被dispose後,全部該subscriber相關的工做都會被中止或取消,如http請求,資源也會被釋放。

Signal events是線性的,不會出現併發的狀況,除非顯示地指定Scheduler。因此-subscribeNext:error:completed:裏的block不須要鎖定或者synchronized等操做,其餘的events會依次排隊,直到block處理完成。

Errors有優先權,若是有多個signals被同時監聽,只要其中一個signal sendError,那麼error就會馬上被傳送給subscriber,並致使signals終止執行。至關於Exception。

生成Signal時,最好指定Name, -setNameWithFormat: 方便調試。

block代碼中不要阻塞。

更多內容

實例

例1. 監聽對象的成員變量變化,當成員變量值被改變時,觸發作一些事情。

    場景1:當前類有一個屬性 NSString *input,當它的值被改變時,發送一個請求。

[RACObserve(self, input)
    subscribeNext:^(id x){
        request(x);//發送一個請求
   }];

    場景2:當前類有一個屬性textField,當它的Text值被改變時,發送一個請求。

[RACObserve(self, textField.text) subscribeNext:^(id x) {
        request(x);//發送一個請求
    }];

     場景1和2 注意:RACObserve(self, textField.text)中的第二參數必定要和當前類的屬性相關,不能是全局變量或者局部變量。subscribeNext:^(id x)中就是RACObserve(self, textField.text)中的第二參數即textField.text。

 

      場景3:在上面場景中,當用戶輸入的值以2開頭時,才發請求.

[[RACObserve(self, input)
     filter:^(NSString* value){
         if ([value hasPrefix:@"2"]) {
             return YES;
         } else {
             return NO;
         }
     }]
     subscribeNext:^(NSString* x){
        request(x);//發送一個請求
    }];

         場景4:面場景是監聽本身的成員變量,若是想監聽UITextField輸入值變化,框架也作了封裝能夠代替系統回調

[[self.priceInput.rac_textSignal
     filter:^BOOL(NSString *str) {
         if (str.integerValue > 20) {
             return YES;
         } else {
             return NO;
         }
     }]
     subscribeNext:^(NSString *str) {
              request(x);//發送一個請求
}]; 

 

例2. 同時監聽多個變量變化,當這些變量知足必定條件時,使button爲可點擊狀態

           場景1:button監聽 兩個輸入框有值和一個屬性Bool變量值,當輸入框有輸入且Bool爲真時,button爲可點擊狀態

RAC(self.payButton,enabled) = [RACSignal
                                   combineLatest:@[self.priceInput.rac_textSignal,
                                                self.nameInput.rac_textSignal,
                                                RACObserve(self, isConnected)
                                                ]
                                   reduce:^(NSString *price, NSString *name, NSNumber *connect){
                                   return @(price.length > 0 && name.length > 0 && [connect boolValue]);
                                   }];

         場景1注意 combineLatest: reduce:中的關係層次和返回值.

          場景2:知足上面條件時,直接發送請求

 

[[RACSignal
                                   combineLatest:@[self.priceInput.rac_textSignal,
                                                self.nameInput.rac_textSignal,
                                                RACObserve(self, isConnected)
                                                ]
                                   reduce:^(NSString *price, NSString *name, NSNumber *connect){
                                   return @(price.length > 0 && name.length > 0 && ![connect boolValue]);
                                   }]
                             subscribeNext:^(NSNumber *res){
                                 if ([res boolValue]) {
                                     NSLog(@"XXXXX send request");
                                 }
                             }];

 

                場景4:用戶每次在TextField中輸入一個字符,1秒內沒有其它輸入時,去發一個請求。TextField中字符改變觸發事件已在例1中展現,這裏實現一下它觸法的方法,把1秒延時在此方法中實現。

- (void)showLoading {
 
    [self.loadingDispose dispose];//上次信號還沒處理,取消它(距離上次生成還不到1秒)
    @weakify(self);
    self.loadingDispose = [[[RACSignal createSignal:^RACDisposable *(id<racsubscriber> subscriber) {
        [subscriber sendCompleted];
        return nil;
    }] delay:1] //延時一秒
    subscribeCompleted:^{
        @strongify(self);
        doRequest();
        self.loadingDispose = nil;
    }];
}
相關文章
相關標籤/搜索