iOS開發,一篇寫得很是好的入門級ReactiveCocoa教程(二)

ReactiveCocoa入門教程——第二部分html

ReactiveCocoa iOS 翻譯    2015-05-20 16:37:16    4710    1    1react

本文翻譯自RayWenderlich  ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2ios

 

ReactiveCocoa是一個框架,它能讓你在iOS應用中使用函數響應式編程(FRP)技術。在本系列教程的第一部分中,你學到了如何將標準的動做與事件處理邏輯替換爲發送事件流的信號。你還學到了如何轉換、分割和聚合這些信號。git

 

在本系列教程的第二部分,你將會學到一些ReactiveCocoa的高級功能,包括:github

•另外兩個事件類型:errorcompleted編程

•節流json

•線程api

•延伸app

•其餘框架

 

是時候深刻研究一下了。

Twitter Instant

在本教程中你將要開發的應用叫Twitter Instant(基於Google Instant的概念),這個應用能搜索Twitter上的內容,並根據輸入實時更新搜索結果。

 

這個應用的初始工程包括一些基本的UI和必須的代碼。和第一部分同樣,你須要使用CocoaPods來獲取ReactiveCocoa框架,並集成到項目中。初始工程已經包含必須的Podfile,因此打開終端,執行下面的命令:

pod install

 

若是執行正確的話,你能看到和下面相似的輸出:

Analyzing dependencies 

Downloading dependencies 

Using ReactiveCocoa (2.1.8) 

Generating Pods project 

Integrating client project

 

這會生成一個Xcode workspcae,TwitterInstant.xcworkspace 。在Xcode中打開它,確認其中包含兩個項目:

TwitterInstant :應用的邏輯就在這裏。

Pods :這裏是外部依賴。目前只包含ReactiveCocoa。

 

構建運行,就能看到下面的界面:


 

花一些時間來熟悉應用的代碼。這個是一個很簡單的應用,基於split view controller。左欄是RWSearchFormViewController,它經過storyboard在上面添加了一些UI控件,經過outlet鏈接了search text field。右欄是RWSearchResultsViewController,目前只是UITableViewController的子類。

 

打開RWSearchFormViewController.m,能看到在viewDidLoad方法中,首先定位到results view controller,而後把它分配給resultsViewController私有屬性。應用的主要邏輯都會集中在RWSearchFormViewController,這個屬性能把搜索結果提供給RWSearchResultsViewController

 

驗證搜索文本的有效性

首先要作的就是驗證搜索文本,來確保文本長度大於2個字符。若是你完成了本系列教程的第一部分,那這個應該很熟悉。

 

RWSearchFormViewController.m中的viewDidLoad 下面添加下面的方法:

- (BOOL)isValidSearchText:(NSString *)text {

    return text.length > 2;

}

 

這個方法就只是確保要搜索的字符串長度大於2個字符。這個邏輯很簡單,你可能會問「爲何要在工程文件中寫這麼一個單獨的方法呢?」。

 

目前驗證輸入有效性的邏輯的確很簡單,但若是未來邏輯須要變得更復雜呢?若是是像上面的例子中那樣,那你就只須要修改一個地方。並且這樣寫能讓你代碼的可讀性更高,代碼自己就說明了你爲何要檢查字符串的長度。

 

在RWSearchFormViewController.m的最上面,引入ReactiveCocoa:

#import <ReactiveCocoa.h>


把下面的代碼加到viewDidLoad的最下面 :

[[self.searchText.rac_textSignal 

    map:^id(NSString *text) {

        return [self isValidSearchText:text] ? 

            [UIColor whiteColor] : [UIColor yellowColor];

    }] 

    subscribeNext:^(UIColor *color) { 

        self.searchText.backgroundColor = color; 

}];

 

上面的代碼作了什麼呢?

•獲取search text field 的text signal

•將其轉換爲顏色來標示輸入是否有效

•而後在subscribeNext:block裏將顏色應用到search text field的backgroundColor屬性

 

構建運行,觀察在輸入文本太短時,text field的背景會變成黃色來標示輸入無效。

 

 

用圖形來表示的話,流程和下面的相似:

 

 

當text field中的文字每次發生變化時,rac_textSignal都會發送一個next 事件,事件包含當前text field中的文字。map這一步將文本值轉換成了顏色值,因此subscribeNext:這一步會拿到這個顏色值,並應用在text field的背景色上。

 

你應該還記得本系列教程第一部分裏這些內容吧?若是忘了,建議你先停在這裏,回去看一下第一部分。

 

在添加Twitter搜索邏輯以前,還有一些有意思的話題要說說。

 

格式化代碼

當你在探索如何格式化ReactiveCocoa的代碼時,慣例是每一個操做新起一行,垂直對齊每一個步驟。

 

在下圖中你能看到比較複雜的代碼是如何對齊的,這是第一部分教程中的代碼。

 

 

這樣對齊能讓你很容易的看到每一步的操做。同時你還應該減小每一個block中的代碼量,若是block中的代碼超過幾行時,就應該新寫一個私有方法。

 

很不幸的是,Xcode不是很喜歡這種風格的格式化,因此你會發現Xcode的自動縮進邏輯老是和你過不去。 

 

內存管理

看一下你添加到TwitterInstant中的代碼,你是否好奇建立的這些管道是如何持有的呢?顯然,它並無分配給某個變量或是屬性,因此它也不會有引用計數的增長,那它是怎麼銷燬的呢?

 

ReactiveCocoa設計的一個目標就是支持匿名生成管道這種編程風格。到目前爲止,在你所寫的全部響應式代碼中,這應該是很直觀的。

 

爲了支持這種模型,ReactiveCocoa本身持有全局的全部信號。若是一個signal有一個或多個訂閱者,那這個signal就是活躍的。若是全部的訂閱者都被移除了,那這個信號就能被銷燬了。更多關於ReactiveCocoa如何管理這一過程,參見文檔Memory Management

 

上面說的就引出了最後一個問題:如何取消訂閱一個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; 

    }]; 

    

// at some point in the future ... 

[subscription dispose];

 

你會發現這個方法並不經常使用到,可是仍是有必要知道能夠這樣作。

 

注意:根據上面所說的,若是你建立了一個管道,可是沒有訂閱它,這個管道就不會執行,包括任何如doNext: block的附加操做。

 

避免循環引用

ReactiveCocoa已經在幕後作了不少事情,這也就意味着你並不須要太多關注signal的內存管理。可是還有一個很重要的內存相關問題你須要注意。

 

看一下你剛纔添加的代碼:

[[self.searchText.rac_textSignal 

    map:^id(NSString *text) { 

        return [self isValidSearchText:text] ? 

            [UIColor whiteColor] : [UIColor yellowColor]; 

    }] 

    subscribeNext:^(UIColor *color) { 

        self.searchText.backgroundColor = color; 

    }];

 

subscribeNext:block中使用了self來獲取text field的引用。block會捕獲並持有其做用域內的值。所以,若是self和這個信號之間存在一個強引用的話,就會形成循環引用。循環引用是否會形成問題,取決於self對象的生命週期。若是self的生命週期是整個應用運行時,好比說本例,那也就無傷大雅。可是在更復雜一些的應用中,就不是這麼回事了。

 

爲了不潛在的循環引用,Apple的文檔Working With Blocks中建議獲取一個self的弱引用。用本例來講就是下面這樣的:

__weak RWSearchFormViewController *bself = self; // Capture the weak reference 


[[self.searchText.rac_textSignal 

    map:^id(NSString *text) { 

        return [self isValidSearchText:text] ? 

            [UIColor whiteColor] : [UIColor yellowColor]; 

    }] 

    subscribeNext:^(UIColor *color) { 

        bself.searchText.backgroundColor = color; 

    }];

 

在上面的代碼中,__weak修飾符使bself成爲了self的一個弱引用。注意如今subscribeNext:block中使用bself變量。不過這種寫法看起來不是那麼優雅。

 

ReactiveCocoa框架包含了一個語法糖來替換上面的代碼。在文件頂部添加下面的代碼:

#import "RACEXTScope.h"

 

而後把代碼替換成下面的:

@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; 

    }];

 

上面的@weakify 和 @strongify 語句是在Extended Objective-C庫中定義的宏,也被包括在ReactiveCocoa中。@weakify宏讓你建立一個弱引用的影子對象(若是你須要多個弱引用,你能夠傳入多個變量),@strongify讓你建立一個對以前傳入@weakify對象的強引用。

 

注意:若是你有興趣瞭解@weakify 和 @strongify 實際上作了什麼,在Xcode中,選擇Product -> Perform Action -> Preprocess 「RWSearchForViewController」。這會對view controller 進行預處理,展開全部的宏,以便你能看到最終的輸出。

 

最後須要注意的一點,在block中使用實例變量時請當心謹慎。這也會致使block捕獲一個self的強引用。你能夠打開一個編譯警告,當發生這個問題時能提醒你。在項目的build settings中搜索「retain」,找到下面顯示的這個選項:

 

 

好了,你已經經過理論的考驗,祝賀你。如今你應該可以開始有意思的部分了:爲你的應用添加一些真正的功能!

 

注意:大家中一些眼尖的讀者,那些關注了上一篇教程的讀者,無疑已經注意到能夠在目前的管道中移除subscribeNext:block,轉而使用RAC宏。若是你發現了這個,修改代碼,而後獎勵本身一個小星星吧~

 

請求訪問Twitter

你將要使用Social Framework來讓TwitterInstant應用能搜索Twitter的內容,使用Accounts Framework來獲取Twitter的訪問權限。關於Social Framework的更詳細內容,參見iOS 6 by Tutorials中的相關章節。

 

在你添加代碼以前,你須要在模擬器或者iPad真機上輸入Twitter的登陸信息。打開設置應用,選擇Twitter選項,而後在屏幕右邊的頁面中輸入登陸信息。

 

 

初始工程已經添加了須要的框架,因此你只需引入頭文件。在RWSearchFormViewController.m中,添加下面的引用。

#import <Accounts/Accounts.h>

#import <Social/Social.h> 

 

就在引用的下面,添加下面的枚舉和常量:

typedef NS_ENUM(NSInteger, RWTwitterInstantError) {

    RWTwitterInstantErrorAccessDenied, 

    RWTwitterInstantErrorNoTwitterAccounts, 

    RWTwitterInstantErrorInvalidResponse 

}; 


static NSString * const RWTwitterInstantDomain = @"TwitterInstant";

 

一下子你就要用到它們來標示錯誤。

 

仍是在這個文件中,在已有屬性聲明的下面,添加下面的代碼:

@property (strong, nonatomic) ACAccountStore *accountStore; 

@property (strong, nonatomic) ACAccountType *twitterAccountType;

 

ACAccountsStore類能讓你訪問你的設備能鏈接到的多個社交媒體帳號,ACAccountType類則表明帳戶的類型。

 

仍是在這個文件中,把下面的代碼添加到viewDidLoad的最下面:

self.accountStore = [[ACAccountStore alloc] init]; 

self.twitterAccountType = [self.accountStore 

    accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

 

上面的代碼建立了一個account store和Twitter帳戶標識符。

 

當應用獲取訪問社交媒體帳號的權限時,用戶會看見一個彈框。這是一個異步操做,所以把這封裝進一個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; 

    }]; 

}

 

這個方法作了下面幾件事:

1定義了一個error,當用戶拒絕訪問時發送。

2和第一部分同樣,類方法createSignal返回一個RACSignal實例。

3經過account store請求訪問Twitter。此時用戶會看到一個彈框來詢問是否容許訪問Twitter帳戶。

4在用戶容許或拒絕訪問以後,會發送signal事件。若是用戶容許訪問,會發送一個next事件,緊跟着再發送一個completed事件。若是用戶拒絕訪問,會發送一個error事件。

 

回憶一下教程的第一部分,signal能發送3種不一樣類型的事件:

•Next

•Completed

•Error

 

在signal的生命週期中,它可能不發送事件,發送一個或多個next事件,在這以後還能發送一個completed事件或一個error事件。

 

最後,爲了使用這個signal,把下面的代碼添加到viewDidLoad的最下面:

[[self requestAccessToTwitterSignal] 

    subscribeNext:^(id x) { 

        NSLog(@"Access granted"); 

    } error:^(NSError *error) { 

        NSLog(@"An error occurred: %@", error); 

    }];

 

構建運行,應該能看到下面這樣的提示:


 

若是你點擊OK,控制檯裏就會顯示subscribeNext:block中的log信息了。若是你點擊Don't Allow,那麼錯誤block就會執行,而且打印相應的log信息。

 

Acounts Framework會記住你的選擇。所以爲了測試這兩個選項,你須要經過 iOS Simulator -> Reset Contents and Settings 來重置模擬器。這個有點麻煩,由於你還須要再次輸入Twitter的登陸信息。

 

連接signal

一旦用戶容許訪問Twitter帳號(但願如此),應用就應該一直監測search text filed的變化,以便搜索Twitter的內容。

 

應用應該等待獲取訪問Twitter權限的signal發送completed事件,而後再訂閱text field的signal。按順序連接不一樣的signal是一個常見的問題,可是ReactiveCocoa處理的很好。

 

把viewDidLoad中當前管道的代碼替換成下面的:

[[[self requestAccessToTwitterSignal] 

    then:^RACSignal *{ 

        @strongify(self) 

        return self.searchText.rac_textSignal; 

    }] 

    subscribeNext:^(id x) { 

        NSLog(@"%@", x); 

    } error:^(NSError *error) { 

        NSLog(@"An error occurred: %@", error); 

    }];

 

 

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

 

注意:你在以前的代碼中已經把self轉成弱引用了,因此就不用在這個管道以前再寫@weakify(self)了。

 

then方法會跳過error事件,所以最終的subscribeNext:error:  block仍是會收到獲取訪問權限那一步發送的error事件。

 

構建運行,而後容許訪問,你應該能看到search text field的輸入會在控制檯裏輸出。

2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m 

2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma 

2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag 

2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi 

2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic 

2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

 

接下來,在管道中添加一個filter操做來過濾掉無效的輸入。在本例裏就是長度不夠3個字符的字符串:

[[[[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); 

    }];

 

 再次構建運行,觀察過濾器的工做:

2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi 

2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic 

2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!

 

如今用圖形來表示管道,就和下圖相似:

 

 

管道從requestAccessToTwitterSignal 開始,而後轉換爲rac_textSignal。同時,next事件經過一個filter,最終到達訂閱者的block。你還能看到第一步發送的error事件也是由subscribeNext:error:block來處理的。

 

如今你已經有了一個發送搜索文本的signal了,是時候來搜索Twitter的內容了。你如今以爲還好嗎?我以爲應該還不錯哦~

 

搜索Twitter的內容

你可使用Social Framework來獲取Twitter搜索API,但的確如你所料,Social Framework不是響應式的。那麼下一步就是把所需的API調用封裝進signal中。你如今應該熟悉這個過程了。

 

RWSearchFormViewController.m中,添加下面的方法:

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

}

 

 方法建立了一個請求,請求經過v1.1 REST API來搜索Twitter。上面的代碼使用q這個搜索參數來搜索Twitter中包含有給定字符串的微博。你能夠在Twitter API 文檔中來閱讀更多關於搜索API和其餘傳入參數的信息。

 

 下一步是基於這個請求建立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 send

相關文章
相關標籤/搜索