1. 程式人生 > >ReactiveCocoa 響應式函數編程

ReactiveCocoa 響應式函數編程

json send lur cti 發出 ted length 能力 ict

簡介


ReactiveCocoa(簡稱為RAC),RAC具有函數響應式編程特性,由Matt Diephouse開源的一個應用於iOS和OS X的新框架。

為什麽使用RAC?


因為RAC具有高聚合低耦合的思想所以使用RAC會讓代碼更簡潔,邏輯更清晰。

如何在項目中添加RAC?


  • 方法1.可以使用Cocoapods導入RAC
    在Podfile中添加如下內容

pod ‘ReactiveObjC‘

  • 其他方法看最下方官方鏈接

工作原理


技術分享圖片 工作原理

常見類解釋


1. Stream - 信號流值 - RACStream類
表示一個基本單元可以為任意值,其值會隨著事件的變化而變化,可以在其上進行一些復雜的操作運算(map,filter,skip,take等.)此類不會被經常使用, 多情況下表現為signal和sequences(RACSignal 和RACSequence繼承於RACStream類)

[[RACObserve(self, reactiveString)
    filter:^BOOL(NSString *value) {
        return [value hasPrefix:@"A"];
}]
subscribeNext:^(NSString *value) {
        NSLog(@"%@",value);
}];

2. Signals - 信號 - RACSignal類

技術分享圖片 RACSignal能力

什麽是Signals?


技術分享圖片 Signals

有訂閱者監聽時信號才會發信息, Signals會向那個訂閱者發送0或多個載有數值的”next”事件,後面跟著一個”complete”事件或一個”error”事件。
Signals會發送三種不同信號給Subscriber

  • next:是可以為nil的新值, RACStream方法只能在這個值上進行操作運算。
  • error:表示在Signals完成之前發生了錯誤,值不會在RACStream類中存儲。
  • completed:表示Signals成功的完成,值不會在RACStream類中存儲。
技術分享圖片 訂閱者監聽
__block int aNumber = 0;
// Signal that will have the side effect of incrementing `aNumber` block
// variable for each subscription before sending it.
RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    aNumber++;
    [subscriber sendNext:@(aNumber)];
    [subscriber sendCompleted];
    return nil;
}];
        
// This will print "subscriber one: 1"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber one: %@", x);
}];
        
// This will print "subscriber two: 2"
[aSignal subscribeNext:^(id x) {
    NSLog(@"subscriber two: %@", x);
}];

如果需要對信號進行過濾,轉換,分解和合並那些值的話則不同的訂閱者可能需要使用信號通過不同方式發送的值。


技術分享圖片 信號處理
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, usernameValid);
RAC(self.Button, alpha) = [usernameIsValidSignal
    map:^(NSNumber *valid) {
        return valid. boolValue?@1:@0.5;
}];

3. Subscriber - 訂閱者 - RACSubscriber協議
表示能夠接收信號的對象,訂閱信號才會激活信號,實現RACSubscriber協議的對象都可以為訂閱者。
可以通過- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock 方法創建Subscriber。

RACSignal *repeatSignal = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] repeat];
[repeatSignal subscribeNext: ^(NSDate* time){
      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
      [formatter setDateFormat:@"HH:mm:ss"];
      NSLog(@"%@",[formatter stringFromDate:time]);
}];

4. Subjects - 手動控制信號 - RACSubject
表示可以手動控制信號,
處理流程:創建信號-訂閱信號-發送信號

// 1.創建信號
RACSubject *subject = [RACSubject subject];
// 2.訂閱信號 First
[subject subscribeNext:^(id x) {
    // block調用時刻:當信號發出新值,就會調用.
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 2.訂閱信號 Second
[subject subscribeNext:^(id x) {
      // block調用時刻:當信號發出新值,就會調用.
      NSLog(@"SecondSubscribeNext%@",x);
}];
// 3.發送信號
[subject sendNext:@"1"];
[subject sendNext:@"2"];

也是RAC代碼與非RAC代碼的Bridge 所以非常有用,此類繼承於RACSignal類。

5. ReplaySubject - 手動回放控制信號 - RACReplaySubject
表示可以手動控制信號,底層實現和RACSubject不一樣,它會先把值保存起來,然後遍歷剛剛保存的所有訂閱者,一個一個調用訂閱者的nextBlock然後調用subscribeNext訂閱信號,遍歷保存的所有值,一個一個調用訂閱者的nextBlock。
可以有以下兩種處理流程:

處理流程 1:創建信號-訂閱信號-發送信號(和Subjects一樣)
處理流程 2:創建信號-發送信號-訂閱信號

// 1.創建信號
RACReplaySubject *replaySubject = [RACReplaySubject subject];
// 2.發送信號
[replaySubject sendNext:@"1"];
[replaySubject sendNext:@"2"];
// 3.訂閱信號 First
[replaySubject subscribeNext:^(id x) {
      NSLog(@"FirstSubscribeNext%@",x);
}];
// 3.訂閱信號 Second
[replaySubject subscribeNext:^(id x) {
      NSLog(@"SecondSubscribeNext%@",x);
}];

6. Command- 命令信號 - RACCommand
表示訂閱響應Action信號,通常由UI來出發,比如一個Button當控件被觸發時會被自動禁用掉。

UIButton *reactiveBtn = [[UIButton alloc] init];
[reactiveBtn setTitle:@"點我" forState:UIControlStateNormal];
    reactiveBtn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(UIButton *input) {
    NSLog(@"點擊了我:%@",input.currentTitle);
    //返回一個空的信號量
    return [RACSignal empty];
}];

7. Sequences- 集合 - RACSequence
表示一個不可變的序列值且不能包含空值,使用-rac_sequence.signal來獲取Signal。

RACSignal *signal = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;
// Outputs
[signal subscribeNext:^(NSString *x) {
    NSLog(@"%@", x);
}];

8. Disposables- 清理訂閱 - RACDisposable
表示用於取消信號的訂閱,當一個signal被subscriber後,當執行sendComplete或sendError時subscriber會被移除,或者手動調用[disposable dispose]進行移除操作。
當subscriber被移除後,所有該subscriber相關的工作都會被停止或取消,如http請求,資源也會被釋放。

9. Scheduler- 計劃 - RACScheduler
表示一個信號隊列,是信號執行任務時所在的隊列或者信號執行完成後將結果放到隊列裏執行,它支持取消對列裏的執行並總是串行執行。

RAC常用宏


RACObserve(TARGET, KEYPATH)
表現形式:RACObserve(self, stringProperty)
KVO的簡化版本 相當於對TARGET中KEYPATH的值設置監聽,返回一個RACSignal

RAC(TARGET, ...)
表現形式:RAC(self, stringProperty) = TextField.rac_textSignal
第一個是需要設置屬性值的對象,第二個是屬性名
RAC宏允許直接把信號的輸出應用到對象的屬性上
每次信號產生一個next事件,傳遞過來的值都會應用到該屬性上

RACChannelTo(TARGET, ...)
RACChannelTo 用於雙向綁定
RACChannelTo(self, stringProperty)=RACChannelTo(self.label, text) ;

RAC結構圖


技術分享圖片 RAC結構圖

RAC基礎使用


創建一個TextField名為usernameTextField 設置監聽TextField

[self.usernameTextField.rac_textSignal 
    subscribeNext:^(id x){
    NSLog(@"%@", x);
}];

filter:如果想添加一個條件 只輸出x的長度大於3的,可以使用filter操作來實現這個目的

[self.usernameTextField.rac_textSignal
    filter:^BOOL(NSString* text){
    return text.length > 3;
}];
技術分享圖片 filter

map:把text轉換成length進行輸出,使用map可以對信號進行轉換,一個源信號轉換成另外一個新的信號輸出

[[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
  return @(text.length);
}]
filter:^BOOL(NSNumber*length){
  return[length integerValue] > 3;
}]
subscribeNext:^(id x){
  NSLog(@"%@", x);
}];
技術分享圖片 map

信號可聚合也可以分割

聚合: 多個信號可以聚合成一個新的信號,這個可以是任何類型的信號

RACSignal *signal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                    reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
                      return @([usernameValid boolValue]&&[passwordValid boolValue]);
                    }];

分割:一個信號可以有很多subscriber,也就是作為很多後續步驟的源

RACSignal *signal = self.usernameTextField.rac_textSignal;
    [signal subscribeNext:^(id x) {
        NSLog(@"1111");
    }];
    [signal subscribeNext:^(id x) {
        NSLog(@"2222");
    }];
}

RAC設置Button的ControlEvents

[[self.signInButton
     rac_signalForControlEvents:UIControlEventTouchUpInside]
     subscribeNext:^(id x) {
     NSLog(@"button click");
}];
技術分享圖片 rac_signalForControlEvents
登陸功能舉例說明
需要實現登陸功能需要點擊登陸button
- (RACSignal *)signInSignal {
    return [RACSignal createSignal:^RACDisposable *(id subscriber){
     [self.signInService 
     signInWithUsername:self.usernameTextField.text
               password:self.passwordTextField.text
               complete:^(BOOL success){
                    [subscriber sendNext:@(success)];
                    [subscriber sendCompleted];
       }];
     return nil;
    }];
}
[[[[self.signInButton
     rac_signalForControlEvents:UIControlEventTouchUpInside]
     doNext:^(id x){
       self.signInButton.enabled =NO;
       self.signInFailureText.hidden =YES;
     }]

    flattenMap:^id(id x){        
        return[self signInSignal];
    }]

    subscribeNext:^(NSNumber*signedIn){
    self.signInButton.enabled =YES;
    BOOL success =[signedIn boolValue];
    self.signInFailureText.hidden = success;
    if(success){
        [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
}];

flattenMap:[self signInSignal]返回的也是signal,所以是信號中的信號,使用這個操作把按鈕點擊事件轉換為登錄信號,同時還從內部信號發送事件到外部信號。

doNext:為一個附加操作,在一個next事件發生時執行的邏輯,而該邏輯並不改變事件本身。

技術分享圖片 流程

RAC高級使用


error 和 completed,節流,線程,延伸,其他

內存管理

ReactiveCocoa設計的一個目標就是支持匿名生成管道這種編程風格。到目前為止,在你所寫的所有響應式代碼中,這應該是很直觀的。
為了支持這種模型,ReactiveCocoa自己持有全局的所有信號。如果一個signal有一個或多個訂閱者,那這個signal就是活躍的。如果所有的訂閱者都被移除了,那這個信號就能被銷毀了。

如何取消訂閱一個signal?
在一個completed或者error事件之後,訂閱會自動移除。你還可以通過RACDisposable 手動移除訂閱。

RACSignal的訂閱方法都會返回一個RACDisposable實例,它能讓你通過dispose方法手動移除訂閱。這個方法並不常用到,但是還是有必要知道可以這樣做。

RACSignal *backgroundColorSignal =
  [self.searchText.rac_textSignal 
      map:^id(NSString *text) { 
          return [self isValidSearchText:text] ? 
              [UIColor whiteColor] : [UIColor yellowColor]; 
  }]; 

RACDisposable *subscription = 
[backgroundColorSignal 
    subscribeNext:^(UIColor *color) {
        self.searchText.backgroundColor = color; 
}]; 

[subscription dispose];?

避免循環引用
在ReactiveCocoa中提供了避免循環引用的方法
@weakify宏讓你創建一個弱引用的影子對象(如果你需要多個弱引用,你可以傳入多個變量),
@strongify讓你創建一個對之前傳入@weakify對象的強引用。

@weakify(self) 
[[self.searchText.rac_textSignal 
map:^id(NSString *text) { 
    return [self isValidSearchText:text] ? 
        [UIColor whiteColor] : [UIColor yellowColor]; 
}] 
subscribeNext:^(UIColor *color) { 
    @strongify(self) 
    self.searchText.backgroundColor = color; 
}];?

signal能發送3種不同類型的事件
Next
Completed
Error

當應用獲取訪問社交媒體賬號的權限時,用戶會看見一個彈框。這是一個異步操作,因此把這封裝進一個signal是很好的選擇

-(RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error 
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                           code:RWTwitterInstantErrorAccessDenied 
                                       userInfo:nil];
                                   
// 2 - create the signal 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    // 3 - request access to twitter 
    @strongify(self) 
    [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType 
           options:nil 
        completion:^(BOOL granted, NSError *error) {
        // 4 - handle the response 
        if (!granted) { 
           [subscriber sendError:accessError]; 
        } else { 
            [subscriber sendNext:nil]; 
            [subscriber sendCompleted]; 
        } 
    }]; 
return nil; 
}]; 
}?

then:then方法會等待completed事件的發送,然後再訂閱由then block返回的signal。這樣就高效地把控制權從一個signal傳遞給下一個。

[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];?
技術分享圖片 then

實時搜索內容方法

  • 創建請求鏈接方法

-(SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/1.1/search/tweets.json"]; 
NSDictionary *params = @{@"q" : text}; 
SLRequest *request = [SLRequest   requestForServiceType:SLServiceTypeTwitter 
                                    requestMethod:SLRequestMethodGET 
                                              URL:url 
                                       parameters:params]; 
return request; 
}?
  • 創建請求signal
-(RACSignal *)signalForSearchWithText:(NSString *)text { 
// 1 - define the errors 
NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                               code:RWTwitterInstantErrorNoTwitterAccounts 
                                           userInfo:nil]; 
NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain 
                                                    code:RWTwitterInstantErrorInvalidResponse 
                                                    userInfo:nil]; 
                                                    
// 2 - create the signal block 
@weakify(self) 
return [RACSignal createSignal:^RACDisposable *(id subscriber) { 
    @strongify(self); 
    
    // 3 - create the request 
    SLRequest *request = [self requestforTwitterSearchWithText:text]; 
    
    // 4 - supply a twitter account 
    NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];       
    if (twitterAccounts.count == 0) { 
        [subscriber sendError:noAccountsError]; 
    } else { 
        [request setAccount:[twitterAccounts lastObject]]; 
        
    // 5 - perform the request 
    [request performRequestWithHandler: ^(NSData *responseData, 
            NSHTTPURLResponse *urlResponse, NSError *error) { 
        if (urlResponse.statusCode == 200) { 
        
            // 6 - on success, parse the response 
            NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData 
                                            options:NSJSONReadingAllowFragments 
                                              error:nil]; 
            [subscriber sendNext:timelineData]; 
            [subscriber sendCompleted]; 
        } else { 
            // 7 - send an error on failure 
            [subscriber sendError:invalidResponseError]; 
        } 
    }]; 
} 
return nil; 
}];
}
  • 使用flattenMap來把每個next事件映射到一個新的signal
[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

線程

在subscribeNext:error:中的數據沒有在主線程(Thread 1)中執行,更新UI只能在主線程中執行,所以更新UI需要轉到主線程中執行。

要怎麽更新UI呢?
通常的做法是使用操作隊列但是ReactiveCocoa有更簡單的解決辦法,在flattenMap:之後添加一個deliverOn:操作就可以轉到主線程上了。
:如果你看一下RACScheduler類,就能發現還有很多選項,比如不同的線程優先級,或者在管道中添加延遲。

[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(id x) { 
    NSLog(@"%@", x); 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

異步加載圖片

-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { 
RACScheduler *scheduler = [RACScheduler 
    schedulerWithPriority:RACSchedulerPriorityBackground]; 
    
return [[RACSignal createSignal:^RACDisposable *(id subscriber) { 
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; 
    UIImage *image = [UIImage imageWithData:data]; 
    [subscriber sendNext:image]; 
    [subscriber sendCompleted]; 
    return nil; 
}] subscribeOn:scheduler]; 
}

首先獲取一個後臺scheduler,來讓signal不在主線程執行。然後,創建一個signal來下載圖片數據,當有訂閱者時創建一個UIImage。最後是subscribeOn:來確保signal在指定的scheduler上執行。

 -(UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
[[[[self signalForLoadingImage:tweet.profileImageUrl] 
takeUntil:cell.rac_prepareForReuseSignal] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(UIImage *image) { 
    cell.twitterAvatarView.image = image; 
}];
return cell;
}

cell是重用的,可能有臟數據,所以上面的代碼首先重置圖片。然後創建signal來獲取圖片數據。你之前也遇到過deliverOn:這一步,它會把next事件發送到主線程,這樣subscribeNext:block就能安全執行了。

cell.rac_prepareForReuseSignal:Cell復用時的清理。
takeUntil:當給定的signal完成前一直取值

節流

每次輸入一個字,搜索都會馬上執行。如果你輸入很快(或者只是一直按著刪除鍵),這可能會造成應用在一秒內執行好幾次搜索,這很不理想。
更好的解決方法是,當搜索文本在短時間內,比如說500毫秒,不再變化時,再執行搜索。
在filter之後添加一個throttle步驟:

[[[[[[[self requestAccessToTwitterSignal] 
then:^RACSignal *{ 
    @strongify(self) 
    return self.searchText.rac_textSignal; 
}] 
filter:^BOOL(NSString *text) { 
    @strongify(self) 
    return [self isValidSearchText:text]; 
}] 
throttle:0.5] 
flattenMap:^RACStream *(NSString *text) { 
    @strongify(self) 
    return [self signalForSearchWithText:text]; 
}] 
deliverOn:[RACScheduler mainThreadScheduler]] 
subscribeNext:^(NSDictionary *jsonSearchResult) { 
    NSArray *statuses = jsonSearchResult[@"statuses"]; 
    NSArray *tweets = [statuses linq_select:^id(id tweet) { 
        return [RWTweet tweetWithStatus:tweet]; 
    }]; 
    [self.resultsViewController displayTweets:tweets]; 
} error:^(NSError *error) { 
    NSLog(@"An error occurred: %@", error); 
}];

throttle:只有當前一個next事件在指定的時間段內沒有被接收到後,throttle操作才會發送next事件。

代替代理

如果想在其他地方監聽到tableView的代理信息則需要設置如下方法

[[tableView rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:) fromProtocol:@protocol(UITableViewDelegate) ] subscribeNext:^(RACTuple * x) {
    NSLog(@"點擊了");
}];

rac_signalForSelector: fromProtocol: 要先綁定在設置代理



作者:PHM
鏈接:https://www.jianshu.com/p/e99cb4310482
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

ReactiveCocoa 響應式函數編程