ReactiveCocoa 教程-權(quán)威介紹/ 第2/2部分

原文:ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2

ReactiveCocoa 是一個(gè)允許你在 iOS 應(yīng)用中使用函數(shù)響應(yīng)式編程(FRP)技術(shù)的框架。通過(guò) ReactiveCocoa 系列教程的第一部分,你學(xué)會(huì)了如何用發(fā)出事件流的信號(hào)來(lái)替換標(biāo)準(zhǔn)的 action 動(dòng)作和事件處理邏輯。你還學(xué)習(xí)了如何轉(zhuǎn)換、分割和組合這些信號(hào)。

在這個(gè)系列的第二部分中,你將學(xué)習(xí) ReactiveCocoa 更高級(jí)的功能。包括:

  • 另外兩個(gè)事件類(lèi)型:errorcompleted
  • Throttling(節(jié)流?)
  • Threading(多線(xiàn)程)
  • Continuations(延續(xù)?)
  • ... 還有更多

是時(shí)候深入挖掘了!

Twitter 實(shí)例

在整個(gè)教程中,你要開(kāi)發(fā)的應(yīng)用程序叫做 Twitter Instant(仿照 Google Instant 的概念),這是一個(gè) Twitter 搜索應(yīng)用程序,可以在你輸入時(shí)實(shí)時(shí)更新搜索結(jié)果。

這個(gè)應(yīng)用程序的初始化項(xiàng)目包括基本的用戶(hù)界面和一些你需要開(kāi)始使用的普通代碼。與第1部分一樣,你需要使用 CocoaPods 來(lái)獲取ReactiveCocoa 框架并將其集成到您的項(xiàng)目中。啟動(dòng)項(xiàng)目已經(jīng)包含了必要的 Podfile,所以打開(kāi)終端窗口并執(zhí)行以下命令:

pod install

如果執(zhí)行正確,你應(yīng)該看到類(lèi)似于下面的輸出。

Analyzing dependencies
Downloading dependencies
Using ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project

這應(yīng)該已經(jīng)生成了一個(gè) Xcode 工作空間,TwitterInstant.xcworkspace。在 Xcode 中打開(kāi)它,確認(rèn)它包含兩個(gè)項(xiàng)目。

  • TwitterInstant:這是你的應(yīng)用程序邏輯所在的地方。
  • Pods: 這是外部依賴(lài)的地方。目前它只包含 ReactiveCocoa。

編譯并運(yùn)行。下面的界面會(huì)迎接你:

image

花點(diǎn)時(shí)間熟悉一下應(yīng)用程序的代碼。這是一個(gè)非常簡(jiǎn)單的基于 split view controller 的應(yīng)用。左側(cè)面板是 RWSearchFormViewController,它通過(guò)故事板添加了一些 UI 控件,搜索文本輸入框連接到一個(gè)插座。右側(cè)面板是 RWSearchResultsViewController,它目前只是一個(gè) UITableViewController 子類(lèi)。

如果你打開(kāi) RWSearchFormViewController.m,你可以看到 viewDidLoad 方法中獲取了結(jié)果視圖控制器,并將其賦值給私有屬性 resultsViewController 。你的大部分應(yīng)用邏輯將體現(xiàn)在 RWSearchFormViewController 中,這個(gè)屬性將向 RWSearchResultsViewController 提供搜索結(jié)果。

驗(yàn)證搜索文本

你要做的第一件事是驗(yàn)證搜索文本,以確保它的長(zhǎng)度大于兩個(gè)字符。如果你完成了本系列的第一部分,這應(yīng)該是一個(gè)愉快的復(fù)習(xí):

- (BOOL)isValidSearchText:(NSString *)text {
    return text.length > 2;
}

這只是確保輸入的搜索字符串長(zhǎng)度大于兩個(gè)字符。有了這樣簡(jiǎn)單的邏輯,你可能會(huì)問(wèn) "為什么要在項(xiàng)目文件中單獨(dú)設(shè)置一個(gè)方法?"

目前的邏輯很簡(jiǎn)單。但如果將來(lái)需要更復(fù)雜的邏輯呢?通過(guò)上面的封裝,你只會(huì)在一個(gè)地方進(jìn)行修改。此外,上面的實(shí)現(xiàn)可以讓你的代碼更有表現(xiàn)力,它表明了你為什么要檢查字符串的長(zhǎng)度。我們都要遵循良好的編碼實(shí)踐,對(duì)嗎?

在同一個(gè)文件的頂部,導(dǎo)入 ReactiveObjC:

#import <ReactiveObjC/ReactiveObjC.h>

在同一個(gè)文件中,在 viewDidLoad 的末尾添加以下內(nèi)容:

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

上面的代碼具體的內(nèi)容:

  • 獲取搜索文本輸入框字段的文本信號(hào)。
  • 將其轉(zhuǎn)換為背景色,表示輸入內(nèi)容是否有效。
  • 然后在 subscribeNext: 塊中將信號(hào)傳遞過(guò)來(lái)的 UIColor 屬性應(yīng)用于輸入框的 backgroundColor 屬性。

編譯并運(yùn)行,觀察當(dāng)搜索輸入框輸入的字符串太短時(shí),輸入框是如何用黃色背景來(lái)表示輸入內(nèi)容無(wú)效的。

image

用流程圖來(lái)說(shuō)明,這個(gè)簡(jiǎn)單的響應(yīng)式管道看起來(lái)有點(diǎn)像這樣:

image

每次輸入內(nèi)容發(fā)生變化時(shí),rac_textSignal 信號(hào)都會(huì)觸發(fā)包含當(dāng)前文本字段文本的 next 事件。map 步驟將文本值轉(zhuǎn)化為顏色,而 subscribeNext: 步驟則將這個(gè)值獲取并應(yīng)用到文本字段的背景上。

當(dāng)然,你還記得第一篇文章中的內(nèi)容吧?如果不記得的話(huà),你可能要在這里停下來(lái),至少讀一遍以練習(xí)。

在添加 Twitter 搜索邏輯之前,還有幾個(gè)有趣的話(huà)題要講。

管道格式化

當(dāng)你深入研究 ReactiveCocoa 代碼的格式化時(shí),普遍接受的慣例是讓每個(gè)操作都在新的行上,并將所有的步驟垂直對(duì)齊。

在下一張圖中,你可以看到一個(gè)更復(fù)雜的例子的對(duì)齊方式,這個(gè)例子取自于之前的教程:

image

這可以讓你非常容易地看到組成管道的操作。另外,盡量減少每個(gè) Block 塊中的代碼量;任何超過(guò)幾行的代碼都應(yīng)該被分解成一個(gè)私有方法。

不幸的是,Xcode 并不喜歡這種格式化的風(fēng)格,所以你可能會(huì)發(fā)現(xiàn)你自己在和它的自動(dòng)縮進(jìn)邏輯作斗爭(zhēng)!

內(nèi)存管理

考慮你添加到 TwitterInstant 應(yīng)用中的代碼,你是否想知道你剛剛創(chuàng)建的管道是如何保留的?當(dāng)然,由于它沒(méi)有被分配到變量或?qū)傩灾校运囊糜?jì)數(shù)不會(huì)遞增,注定要被銷(xiāo)毀?

ReactiveCocoa 的設(shè)計(jì)目標(biāo)之一就是允許這種編程風(fēng)格,管道可以匿名形成。在你到目前為止所寫(xiě)的所有響應(yīng)式代碼中,這看起來(lái)應(yīng)該是很直觀的。

為了支持這種模式,ReactiveCocoa 維護(hù)并保留了自己的全局信號(hào)集。如果它有一個(gè)或多個(gè)訂閱者,那么這個(gè)信號(hào)是活躍的。如果所有的訂閱者都被移除,那么信號(hào)就會(huì)被取消分配。關(guān)于 ReactiveCocoa 如何管理這個(gè)過(guò)程的更多信息,請(qǐng)參見(jiàn)內(nèi)存管理文檔。

最后一個(gè)問(wèn)題: 如何取消訂閱一個(gè)信號(hào)?在 completederror 事件后,訂閱會(huì)自動(dòng)移除(您將很快了解更多信息)。你也可以通過(guò) RACDisposable 來(lái)實(shí)現(xiàn)手動(dòng)刪除信號(hào)。

RACSignal 上的訂閱方法都會(huì)返回一個(gè) RACDisposable 的實(shí)例,允許你通過(guò) dispose 方法手動(dòng)刪除訂閱。下面是一個(gè)使用當(dāng)前管道的快速示例:

// 返回包含 UIColor 實(shí)例的信號(hào)
RACSignal *backgroundColorSignal = [self.searchText.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
    return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}];

// RACSignal 的訂閱方法會(huì)返回一個(gè) RACDisposable 的實(shí)例
RACDisposable *subscription = [backgroundColorSignal subscribeNext:^(UIColor *color) {
    self.searchText.backgroundColor = color;
}];

// 在未來(lái)的某個(gè)時(shí)刻,手動(dòng)刪除信號(hào)的訂閱
[subscription dispose];

你會(huì)發(fā)現(xiàn)自己不可能經(jīng)常這樣做,但值得知道這種可能性的存在。

注意:作為一個(gè)必然的結(jié)果,如果你創(chuàng)建了一個(gè)管道(信號(hào))但沒(méi)有訂閱它,管道永遠(yuǎn)不會(huì)執(zhí)行,也包括任何 side-effects 方法,如 doNext: 塊。

避免引循環(huán)

雖然 ReactiveCocoa 在幕后做了很多聰明的事情——這意味著你不用太擔(dān)心信號(hào)的內(nèi)存管理,但有一個(gè)重要的與內(nèi)存相關(guān)問(wèn)題你確實(shí)需要考慮。

如果你查看你剛剛添加的響應(yīng)式代碼:

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

subscribeNext: 塊使用 self 來(lái)獲取對(duì)文本輸入框的引用。Block 會(huì)捕獲并保留閉包中的值,因此,如果 self 和這個(gè)信號(hào)之間存在強(qiáng)引用,將導(dǎo)致引用循環(huán)問(wèn)題。這是否重要取決于 self 對(duì)象的生命周期。如果它的生命周期是應(yīng)用程序的持續(xù)時(shí)間,就像這里的情況一樣,這并不重要。但在更復(fù)雜的應(yīng)用中,這種情況很少。

為了避免這種潛在的引用循環(huán)問(wèn)題,Apple 的 Working With Blocks 文檔建議捕獲一個(gè)弱引用到 self。在當(dāng)前的代碼中,你可以通過(guò)以下方式實(shí)現(xiàn):

__weak RWSearchFormViewController *weakSelf = self; // 捕獲弱引用

[[self.searchText.rac_textSignal
  map:^id _Nullable (NSString *_Nullable value) {
      return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
  }] subscribeNext:^(UIColor *color) {
      weakSelf.searchText.backgroundColor = color;
  }];

以上代碼中,weakSelf 是對(duì) self 的引用,為了使它成為一個(gè)弱引用,它被標(biāo)記為 __weak。請(qǐng)注意,subscribeNext: 塊現(xiàn)在使用了 weakSelf 變量。這看起來(lái)不是很優(yōu)雅!

ReactiveCocoa 框架中包含了一個(gè)小技巧,你可以用它來(lái)代替上面的代碼。在文件的頂部添加以下導(dǎo)入:

#import "RACEXTScope.h"

然后用下面的代碼代替上面的代碼:

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

上面的 @weakify@strongify 語(yǔ)句是在 Extended Objective-C 庫(kù)中定義的宏,它們也包含在 ReactiveCocoa 中。@weakify 宏允許你創(chuàng)建影子變量(shadow variables),也就是弱引用(如果你需要多個(gè)弱引用,可以傳遞多個(gè)變量),@strongify 宏允許你對(duì)之前傳遞給 @weakify 的變量創(chuàng)建強(qiáng)引用。

注意:如果你有興趣了解 @weakify@strongify 的實(shí)際作用,在 Xcode 中選擇 Product -> Perform Action -> Preprocess "RWSearchForViewController"。這將對(duì)視圖控制器進(jìn)行預(yù)處理,展開(kāi)所有的宏,并允許你看到最終的輸出。

最后需要注意的是,在 Block 塊中使用實(shí)例變量時(shí)要小心。這些也會(huì)導(dǎo)致 Block 塊捕捉到對(duì) self 的強(qiáng)引用。如果是你的代碼導(dǎo)致這個(gè)問(wèn)題,你可以打開(kāi)編譯器警告來(lái)提醒你。在項(xiàng)目的構(gòu)建設(shè)置中搜索 retain,可以找到下面的選項(xiàng):

image

好了,你從理論知識(shí)中幸存下來(lái)了,恭喜你!現(xiàn)在你聰明多了,準(zhǔn)備好進(jìn)入有趣的部分:為你的應(yīng)用程序添加一些真正的功能。

:關(guān)注過(guò)上一篇教程的敏銳讀者無(wú)疑會(huì)注意到,通過(guò)利用 RAC 宏,你可以在當(dāng)前管道中取消對(duì) subscribeNext: 塊的調(diào)用。如果你發(fā)現(xiàn)了這一點(diǎn),請(qǐng)做出這樣的改變,并為自己頒發(fā)一顆閃亮的金星!

// 通過(guò) RAC 宏實(shí)現(xiàn):將信號(hào)的輸出分配給 self.searchText 對(duì)象的 backgroundColor 屬性
RAC(self.searchText, backgroundColor) = [self.searchText.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
    return [self isValidSearchText:value] ? UIColor.whiteColor : UIColor.yellowColor;
}];

請(qǐng)求訪(fǎng)問(wèn) Twitter

你將使用 Social Framework 來(lái)允許 TwitterInstant 應(yīng)用程序搜索 Tweets,并使用 Accounts Framework 來(lái)授予對(duì) Twitter 的訪(fǎng)問(wèn)權(quán)。要想更詳細(xì)地了解 Social Framework,請(qǐng)查看 iOS 6 教程 中專(zhuān)門(mén)為這個(gè)框架編寫(xiě)的章節(jié)。

在添加代碼之前,你需要將你的 Twitter 憑證輸入到模擬器或你正在運(yùn)行這款應(yīng)用的 iPad 上。打開(kāi) Settings 應(yīng)用,選擇 Twitter 菜單選項(xiàng),然后在屏幕右側(cè)添加你的憑證:

image

初始化項(xiàng)目已經(jīng)添加了所需的框架,所以你只需要導(dǎo)入頭文件。在 RWSearchFormViewController.m 文件中,在文件的頂部添加以下代碼:

#import <Accounts/Accounts.h>
#import <Social/Social.h>

import 語(yǔ)句的下面添加以下枚舉和常量:

typedef NS_ENUM(NSUInteger, RWTwitterInstantError) {
    RWTwitterInstantErrorAccessDenied,
    RWTwitterInstantErrorNoTwitterAccounts,
    RWTwitterInstantErrorInvalidResponse,
};

static NSString *const RWTwitterInstantDomain = @"TwitterInstant";

你將很快使用它們來(lái)識(shí)別錯(cuò)誤。在同一個(gè)文件的下面,在現(xiàn)有屬性聲明的下面,添加以下內(nèi)容:

@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;

ACAccountsStore 類(lèi)提供了通過(guò)設(shè)備連接到各種社交媒體賬戶(hù)的訪(fǎng)問(wèn),而 ACAccountType 類(lèi)則代表了一種特定的賬戶(hù)類(lèi)型。

在同一個(gè)文件中再往下,在 viewDidLoad 的末尾添加以下內(nèi)容:

self.accountStore = [[ACAccountStore alloc] init];
// Deprecated,該功能似乎已經(jīng)失效!頭文件中建議使用 Twitter SDK 實(shí)現(xiàn)
self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

這將創(chuàng)建賬戶(hù)管理器(account store)和 Twitter 賬戶(hù)標(biāo)識(shí)符。

當(dāng)一個(gè)應(yīng)用程序請(qǐng)求訪(fǎng)問(wèn)一個(gè)社交媒體賬戶(hù)時(shí),用戶(hù)會(huì)看到一個(gè)彈出窗口。這是一個(gè)異步操作,因此它是一個(gè)很好的信號(hào)包裝的候選者,以便響應(yīng)式地使用它。

在同一個(gè)文件中,再往下添加以下方法:

- (RACSignal *)requestAccessToTwitterSignal {
    // 1. 定義一個(gè)錯(cuò)誤
    NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorAccessDenied userInfo:nil];
    
    // 2. 創(chuàng)建一個(gè)信號(hào)
    @weakify(self)
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        
        // 3. 請(qǐng)求訪(fǎng)問(wèn) Twitter
        @strongify(self)
        [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType options:nil completion:^(BOOL granted, NSError *error) {
            
            // 4. 處理請(qǐng)求響應(yīng)
            if (!granted) {
                [subscriber sendError:accessError];
            } else {
                [subscriber sendNext:nil];
                [subscriber sendCompleted];
            }
        }];
        return nil;
    }];
}

這個(gè)方法的作用如下:

  1. 定義了一個(gè)錯(cuò)誤,如果用戶(hù)拒絕訪(fǎng)問(wèn),就會(huì)發(fā)送錯(cuò)誤。
  2. 參考教程一,類(lèi)方法 createSignal 返回了一個(gè) RACSignal 的實(shí)例。
  3. 通過(guò)賬戶(hù)管理器請(qǐng)求訪(fǎng)問(wèn) Twitter。此時(shí),用戶(hù)會(huì)看到一個(gè)提示,要求他們授予這個(gè)應(yīng)用對(duì)其 Twitter 賬戶(hù)的訪(fǎng)問(wèn)權(quán)。
  4. 在用戶(hù)授予或拒絕訪(fǎng)問(wèn)后,就會(huì)發(fā)出信號(hào)事件。如果用戶(hù)授予訪(fǎng)問(wèn)權(quán),則會(huì)發(fā)出 next 事件,然后是 completed 事件。如果用戶(hù)拒絕訪(fǎng)問(wèn),則會(huì)發(fā)出 error 事件。

如果你還記得我們的第一篇教程,一個(gè)信號(hào)可以發(fā)出三種不同的事件類(lèi)型:

  • Next
  • Completed
  • Error

在一個(gè)信號(hào)的生命周期中,它可能不發(fā)出任何事件,一個(gè)或多個(gè) next 事件,然后緊接著是一個(gè) completed 事件或一個(gè) error 事件。

最后,為了利用這個(gè)信號(hào),在 viewDidLoad 方法的結(jié)尾添加以下內(nèi)容:

[self requestAccessToTwitterSignal] subscribeNext:^(id  _Nullable x) {
    NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
};

如果你編譯并運(yùn)行應(yīng)用,應(yīng)該會(huì)有以下提示:

image

如果你點(diǎn)擊 OK,則應(yīng)在控制臺(tái)中出現(xiàn) subscribeNext: 塊中的日志信息,而如果你點(diǎn)擊 Don't Allow,則會(huì)執(zhí)行 error Block 塊并記錄相應(yīng)的信息。

賬戶(hù)框架會(huì)記住你所做的決定。因此,如果要測(cè)試這兩種不同的選擇,你需要通過(guò) iOS 模擬器->重置內(nèi)容和設(shè)置......菜單選項(xiàng)來(lái)重置模擬器。這個(gè)過(guò)程有點(diǎn)痛苦,因?yàn)槟氵€必須重新輸入你的 Twitter 賬戶(hù)憑證!

鏈接信號(hào)

一旦用戶(hù)同意了(希望如此)應(yīng)用對(duì)其 Twitter 賬戶(hù)的訪(fǎng)問(wèn)權(quán)限,應(yīng)用程序就需要持續(xù)監(jiān)控搜索文本字段的變化,以便查詢(xún) twitter。

應(yīng)用程序需要等待請(qǐng)求訪(fǎng)問(wèn)用戶(hù) Twitter 的信號(hào)發(fā)出 completed 事件,然后訂閱文本字段的信號(hào)。不同信號(hào)的順序鏈?zhǔn)且粋€(gè)常見(jiàn)的問(wèn)題,但 ReactiveCocoa 處理得非常優(yōu)雅。

viewDidLoad 的結(jié)尾處用以下內(nèi)容替換你當(dāng)前的管道:

[[[self requestAccessToTwitterSignal]
  then:^RACSignal * _Nonnull{
    @strongify(self)
    return self.searchText.rac_textSignal;
}] subscribeNext:^(id  _Nullable x) {
    NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
}];

then 方法等到一個(gè) completed 事件發(fā)出后,再訂閱其 Block 塊參數(shù)重返回的信號(hào)。這就有效地將控制權(quán)從一個(gè)信號(hào)傳遞到下一個(gè)信號(hào)。

注意:你已經(jīng)在為位于這個(gè)管道上方的管道中弱化了 self,所以沒(méi)有必要在這個(gè)管道之前使用 @weakify(self)

then 方法也會(huì)將 error 事件傳遞過(guò)去。因此,最后的 subscribeNext:error: 塊仍然會(huì)接收初始訪(fǎng)問(wèn)請(qǐng)求步驟發(fā)出的錯(cuò)誤。

當(dāng)你編譯并運(yùn)行應(yīng)用,然后授予訪(fǎng)問(wèn)權(quán)時(shí),你應(yīng)該看到你輸入到搜索字段的文本記錄在控制臺(tái)中:

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!

接下來(lái),在管道中添加一個(gè) filter 過(guò)濾操作,以刪除任何無(wú)效的搜索字符串。在本例中,它們是由少于三個(gè)字符組成的字符串:

[[[[self requestAccessToTwitterSignal]
  then:^RACSignal * _Nonnull{
    @strongify(self)
    return self.searchText.rac_textSignal;
}] filter:^BOOL(id  _Nullable value) {
    @strongify(self)
    return [self isValidSearchText:value];
}] subscribeNext:^(id  _Nullable x) {
    NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
}];

編譯并再次運(yùn)行,觀察過(guò)濾的運(yùn)行情況:

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!

用圖形化的方式來(lái)說(shuō)明當(dāng)前的應(yīng)用管道,它是這樣的:

image

應(yīng)用流水線(xiàn)從 requestAccessToTwitterSignal 開(kāi)始,然后切換到 rac_textSignal。同時(shí),next 事件通過(guò)過(guò)濾器,最后進(jìn)入訂閱Block 塊。你還可以看到第一步發(fā)出的任何錯(cuò)誤事件都會(huì)被同一個(gè) subscribeNext:error: 塊所消耗。

現(xiàn)在,你已經(jīng)有了一個(gè)發(fā)出搜索文本的信號(hào),是時(shí)候用它來(lái)搜索 Twitter了! 你玩得開(kāi)心嗎?你應(yīng)該是的,因?yàn)楝F(xiàn)在你真的有收獲了。

搜索 Twitter

Social Framework 是訪(fǎng)問(wèn) Twitter 搜索 API 的一個(gè)選項(xiàng)。然而,正如你所預(yù)料的那樣,Social Framework 并不是響應(yīng)式的! 下一步是將所需的 API 方法調(diào)用包裹在一個(gè)信號(hào)中。你現(xiàn)在應(yīng)該已經(jīng)掌握了這個(gè)過(guò)程的竅門(mén)了!

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

這將創(chuàng)建一個(gè)通過(guò) v1.1 REST API 搜索 Twitter 的請(qǐng)求。上面的代碼使用 q 搜索參數(shù)來(lái)搜索包含給定搜索字符串的推文。你可以在 T witter API 文檔中閱讀更多關(guān)于這個(gè)搜索API,以及其他你可以傳遞的參數(shù)。

下一步是根據(jù)這個(gè)請(qǐng)求創(chuàng)建一個(gè)信號(hào)。在同一個(gè)文件中,添加以下方法:

- (RACSignal *)signalForSearchWithText:(NSString *)text {
    // 1. 定義錯(cuò)誤
    NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorNoTwitterAccounts userInfo:nil];
    NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorInvalidResponse userInfo:nil];
    
    // 2. 創(chuàng)建信號(hào) block
    @weakify(self)
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        @strongify(self)
        
        // 3.創(chuàng)建請(qǐng)求
        SLRequest *request = [self requestforTwitterSearchWithText:text];
        
        // 4.請(qǐng)求 twitter 賬戶(hù)
        NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType];
        if (twitterAccounts.count == 0) {
            [subscriber sendError:noAccountsError];
        } else {
            [request setAccount:twitterAccounts.lastObject];
            
            // 5. 執(zhí)行請(qǐng)求
            [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                if (urlResponse.statusCode == 200) {
                    
                    // 6. 一旦請(qǐng)求成功,解析響應(yīng)
                    NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
                    [subscriber sendNext:timelineData];
                    [subscriber sendCompleted];
                } else {
                    // 7. 發(fā)送請(qǐng)求失敗的錯(cuò)誤信號(hào)
                    [subscriber sendError:invalidResponseError];
                }
            }];
        }
        
        return nil;
    }];
}

依次進(jìn)行每一步:

  1. 起初,你需要定義幾個(gè)不同的錯(cuò)誤,一個(gè)表示用戶(hù)沒(méi)有在他們的設(shè)備上添加任何 Twitter 賬戶(hù), 另一個(gè)表示在執(zhí)行查詢(xún)時(shí)出錯(cuò)。
  2. 和之前一樣,創(chuàng)建一個(gè)信號(hào)。
  3. 使用你在上一步添加的方法為給定的搜索字符串創(chuàng)建一個(gè)請(qǐng)求。
  4. 查詢(xún)賬戶(hù)商店,找到第一個(gè)可用的 Twitter 賬戶(hù)。如果沒(méi)有給定賬戶(hù),則會(huì)發(fā)出一個(gè)錯(cuò)誤。
  5. 執(zhí)行該請(qǐng)求。
  6. 在響應(yīng)成功的情況下(HTTP 響應(yīng)狀態(tài)碼 200),返回的 JSON 數(shù)據(jù)將被解析并作為 next 事件一起發(fā)出,隨后是一個(gè) completed 事件。
  7. 如果是不成功的響應(yīng),則會(huì)發(fā)出一個(gè) error 事件。

現(xiàn)在要把這個(gè)新信號(hào)用起來(lái)了!

在本教程的第一部分,你學(xué)習(xí)了如何使用 flattenMap 將每個(gè) next 事件 map 轉(zhuǎn)移到一個(gè)新的信號(hào)上,然后再訂閱。現(xiàn)在是時(shí)候再次使用這個(gè)信號(hào)了。在 viewDidLoad 的結(jié)尾處,通過(guò)在結(jié)尾處添加一個(gè) flattenMap 步驟來(lái)更新你的應(yīng)用管道:

[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal * _Nonnull{
    @strongify(self)
    return self.searchText.rac_textSignal;
}] filter:^BOOL(id  _Nullable value) {
    @strongify(self)
    return [self isValidSearchText:value];
}] flattenMap:^__kindof RACSignal * _Nullable(id  _Nullable value) {
    @strongify(self)
    return [self signalForSearchWithText:value];
}] subscribeNext:^(id  _Nullable x) {
    NSLog(@"Access granted");
} error:^(NSError * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
}];

編譯并運(yùn)行,然后在搜索文本字段中輸入一些文本。一旦文本至少有三個(gè)字符或更多的長(zhǎng)度,你應(yīng)該在控制臺(tái)窗口中看到 Twitter 搜索的結(jié)果。

下面顯示的只是你將看到的數(shù)據(jù)種類(lèi)的一個(gè)片段:

2014-01-05 07:42:27.697 TwitterInstant[40308:5403] {
    "search_metadata" =     {
        "completed_in" = "0.019";
        count = 15;
        "max_id" = 419735546840117248;
        "max_id_str" = 419735546840117248;
        "next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";
        query = asd;
        "refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";
        "since_id" = 0;
        "since_id_str" = 0;
    };
    statuses =     (
                {
            contributors = "<null>";
            coordinates = "<null>";
            "created_at" = "Sun Jan 05 07:42:07 +0000 2014";
            entities =             {
                hashtags = ...

signalForSearchText: 方法也會(huì)發(fā)出錯(cuò)誤事件,subscribeNext:error: 塊會(huì)消耗這些錯(cuò)誤事件。你可以相信我的話(huà),但你可能想測(cè)試一下!

在模擬器內(nèi)打開(kāi) Settings 應(yīng)用,選擇你的 Twitter 賬戶(hù),然后點(diǎn)擊 Delete Account 按鈕刪除它。

image

如果你重新運(yùn)行應(yīng)用程序,它仍然被允許訪(fǎng)問(wèn)用戶(hù)的 Twitter 賬戶(hù),但沒(méi)有賬戶(hù)可用。因此,signalForSearchText 方法會(huì)發(fā)出一個(gè)錯(cuò)誤,并被記錄下來(lái):

2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error 
  Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"

Code=1 表示這是 RWTwitterInstantErrorNoTwitterAccounts 錯(cuò)誤。在生產(chǎn)應(yīng)用中,你會(huì)希望打開(kāi)錯(cuò)誤代碼,做一些更有意義的事情,而不僅僅是記錄結(jié)果。

這說(shuō)明了關(guān)于 error 事件的一個(gè)重要觀點(diǎn),只要一個(gè)信號(hào)發(fā)出錯(cuò)誤,它就會(huì)直接跳到錯(cuò)誤處理 Block 塊。這是一個(gè)特殊的流程。

:當(dāng) Twitter 請(qǐng)求返回錯(cuò)誤時(shí),可以去行使另一個(gè)特殊流程。這里有一個(gè)快速提示,嘗試將請(qǐng)求參數(shù)改為無(wú)效的東西!

線(xiàn)程

我相信你一定心癢難耐,想把 Twitter 搜索返回的 JSON 數(shù)據(jù)輸出并顯示到 UI 中,但在這之前,你還需要做最后一件事。要知道這是什么,你需要做一些探索!

在下面指定的位置為 subscribeNext:error: 步驟添加一個(gè)斷點(diǎn):

image

重新運(yùn)行應(yīng)用程序,如果需要的話(huà),再次重新輸入你的 Twitter 證書(shū),然后在搜索欄中輸入一些文本。當(dāng)斷點(diǎn)到達(dá)時(shí),你應(yīng)該看到類(lèi)似下圖的東西:

image

請(qǐng)注意,調(diào)試器打出中斷的代碼并沒(méi)有在主線(xiàn)程上執(zhí)行,主線(xiàn)程在上面的截圖中顯示為 Thread 1。請(qǐng)記住,最重要的是你只從主線(xiàn)程更新 UI,因此如果你想在 UI 中顯示推文列表,你就必須切換線(xiàn)程。

這說(shuō)明了關(guān)于 ReactiveCocoa 框架的一個(gè)重要觀點(diǎn)。上圖所示的操作會(huì)在信號(hào)最初發(fā)出事件的線(xiàn)程上執(zhí)行。試著在其他管道步驟處添加斷點(diǎn),你可能會(huì)驚訝地發(fā)現(xiàn)它們?cè)诙鄠€(gè)不同的線(xiàn)程上執(zhí)行!

那么你如何去更新 UI 呢?典型的方法是使用操作隊(duì)列(詳見(jiàn)本站其他地方的教程如何使用 NSOperations 和 NSOperationQueues),然而 ReactiveCocoa 有一個(gè)更簡(jiǎn)單的解決方案來(lái)解決這個(gè)問(wèn)題。

更新你的管道,在 flattenMap: 之后添加一個(gè) deliveryOn: 操作,如下所示:

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

現(xiàn)在重新運(yùn)行應(yīng)用程序,并輸入一些文本,使您的應(yīng)用程序擊中斷點(diǎn)。你應(yīng)該看到你的 subscribeNext:error: 塊中的日志語(yǔ)句現(xiàn)在正在主線(xiàn)程上執(zhí)行:

image

啥玩意兒?只需一個(gè)簡(jiǎn)單的操作,就能把事件的流向匯集到不同的線(xiàn)程上?這有多厲害!?

你可以放心地繼續(xù)更新你的 UI 了!

注意:如果你看一下 RACScheduler 類(lèi),你會(huì)發(fā)現(xiàn)有相當(dāng)多的選項(xiàng)可以在不同優(yōu)先級(jí)的線(xiàn)程上交付,或者在管道中添加延遲。

是時(shí)候看看那些推特了!

更新 UI

如果你打開(kāi) RWSearchResultsViewController.h,你會(huì)發(fā)現(xiàn)它已經(jīng)有一個(gè) displayTweets: 方法,它將使右側(cè)視圖控制器渲染提供的推文數(shù)組。實(shí)現(xiàn)非常簡(jiǎn)單,它只是一個(gè)標(biāo)準(zhǔn)的 UITableView 數(shù)據(jù)源。displayTweets: 方法的單一參數(shù)期望一個(gè)包含 RWTweet 實(shí)例的 NSArray。你還會(huì)發(fā)現(xiàn) RWTweet 模型對(duì)象是作為啟動(dòng)項(xiàng)目的一部分提供的。

目前到達(dá) subscibeNext:error: 步驟的數(shù)據(jù)是一個(gè) NSDictionary,它是通過(guò)在 signalForSearchWithText: 方法中解析 JSON 響應(yīng)而構(gòu)建的。那么如何確定這個(gè)字典的內(nèi)容呢?

如果你看一下 Twitter API 文檔,你可以看到一個(gè)樣本響應(yīng)。NSDictionary 鏡像了這個(gè)結(jié)構(gòu),所以你應(yīng)該會(huì)發(fā)現(xiàn)它有一個(gè)名為 status 的鍵,它是一個(gè) NSArraytweets,也是 NSDictionary 實(shí)例。

如果你看一下 RWTweet,它已經(jīng)有一個(gè)類(lèi)方法 tweetWithStatus: 它接收一個(gè)給定格式的 NSDictionary,并提取所需數(shù)據(jù)。所以,你需要做的就是寫(xiě)一個(gè) for 循環(huán),然后在數(shù)組中迭代,為每一條 tweet 創(chuàng)建一個(gè) RWTweet 實(shí)例。

然而,你不會(huì)這么做的! 哦,不,還有更好的東西在等著你呢!

本文介紹的是 ReactiveCocoa 和函數(shù)式編程。當(dāng)你使用函數(shù)式 API 時(shí),將數(shù)據(jù)從一種格式轉(zhuǎn)換為另一種格式會(huì)更加優(yōu)雅。你將使用LinqToObjectiveC 來(lái)執(zhí)行這個(gè)任務(wù)。

關(guān)閉 TwitterInstant 工作區(qū),然后在 TextEdit 中打開(kāi)在第一個(gè)教程中創(chuàng)建的 Podfile。更新文件以添加新的依賴(lài)關(guān)系:

# 指明依賴(lài)庫(kù)的來(lái)源地址,不使用默認(rèn) CDN
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '9.0'

target 'TwitterInstant' do
  pod 'ReactiveObjC', '~> 3.1.1'
  pod 'LinqToObjectiveC', '~> 2.1.0'
end

在同一文件夾中打開(kāi)終端窗口,并發(fā)出以下命令:

pod update

您將看到類(lèi)似于以下內(nèi)容的輸出:

Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.1.0)
Generating Pods project
Integrating client project
Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.

重新打開(kāi) Xcode workspace,驗(yàn)證新的 pod 是否顯示,如下圖所示:

image

打開(kāi) RWSearchFormViewController.m,并在文件頂部添加以下 import 代碼:

#import "RWTweet.h"
#import <NSArray+LinqExtensions.h>

NSArray+LinqExtensions.h 頭文件來(lái)自 LinqToObjectiveC,它為 NSArray 添加了許多方法,允許你使用流暢的 API 對(duì)其數(shù)據(jù)進(jìn)行轉(zhuǎn)換、排序、分組和過(guò)濾。

現(xiàn)在要把這個(gè) API 用起來(lái)......在 viewDidLoad 結(jié)尾處更新當(dāng)前管道,如下所示:

[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal * _Nonnull{
    @strongify(self)
    return self.searchText.rac_textSignal;
}] filter:^BOOL(id  _Nullable value) {
    @strongify(self)
    return [self isValidSearchText:value];
}] flattenMap:^__kindof RACSignal * _Nullable(id  _Nullable value) {
    @strongify(self)
    return [self signalForSearchWithText:value];
}] 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 * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
}];

如上所示,subscribeNext: 塊首先獲取推文的 NSArraylinq_select 方法通過(guò)在每個(gè)數(shù)組元素上執(zhí)行所提供的 Block 塊來(lái)轉(zhuǎn)換NSDictionary 實(shí)例數(shù)組,從而得到一個(gè) RWTweet 實(shí)例數(shù)組。

一旦轉(zhuǎn)換完畢,推文就會(huì)被發(fā)送到結(jié)果視圖控制器。

構(gòu)建并運(yùn)行,最終可以看到推文出現(xiàn)在 UI 中。

image

注:ReactiveCocoa 和 LinqToObjectiveC 的靈感來(lái)源相似。ReactiveCocoa 是以微軟的 Reactive Extensions 庫(kù)為藍(lán)本,而 LinqToObjectiveC 則是以他們的語(yǔ)言集成查詢(xún) API,或者 LINQ,特別是 Linq to Objects 為藍(lán)本。

異步加載圖片

你可能已經(jīng)注意到了,每條推特的左邊都有一個(gè)空隙。這個(gè)空間是用來(lái)顯示 Twitter 用戶(hù)的頭像的。

RWTweet 類(lèi)已經(jīng)有一個(gè) profileImageUrl 屬性,該屬性被填充了一個(gè)合適的 URL 來(lái)獲取這個(gè)圖片。為了使表格視圖能夠順利滾動(dòng),你需要確保從給定的 URL 中獲取這張圖片的代碼不在主線(xiàn)程上執(zhí)行。這可以使用 Grand Central Dispatch 或 NSOperationQueue 來(lái)實(shí)現(xiàn)。但為什么不使用 ReactiveCocoa 呢?

打開(kāi) RWSearchResultsViewController.m,在文件末尾添加以下方法:

// 異步加載圖片
- (RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
    RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground];
    
    return [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
        UIImage *image = [UIImage imageWithData:data];
        [subscriber sendNext:image];
        [subscriber sendCompleted];
        return nil;
    }] subscribeOn:scheduler];
}

現(xiàn)在你應(yīng)該對(duì)這個(gè)模式很熟悉了吧!

上面的方法首先獲得一個(gè)后臺(tái)調(diào)度器,因?yàn)槟阆M@個(gè)信號(hào)在主線(xiàn)程以外的線(xiàn)程上執(zhí)行。接下來(lái),它創(chuàng)建了一個(gè)下載圖像數(shù)據(jù)的信號(hào),并在有訂閱者時(shí)創(chuàng)建一個(gè) UIImage。最后一個(gè)魔法是 subscribeOn:,它確保信號(hào)在給定的調(diào)度器上執(zhí)行。

神奇!

現(xiàn)在,在同一個(gè)文件中,更新 tableView:cellForRowAtIndex: 方法,在 return 語(yǔ)句前添加以下內(nèi)容:

cell.twitterAvatarView.image = nil;

[[[self signalForLoadingImage:tweet.profileImageUrl]
     deliverOn:[RACScheduler mainThreadScheduler]]
     subscribeNext:^(UIImage *image) {
        cell.twitterAvatarView.image = image;
    }];

首先首先重置圖像,因?yàn)檫@些單元格是重復(fù)使用的,因此可能包含陳舊的數(shù)據(jù)。然后創(chuàng)建所需的信號(hào)來(lái)獲取圖像數(shù)據(jù)。你之前遇到的 deliverOn: 管道步驟,將 next 事件調(diào)度到主線(xiàn)程上執(zhí)行,這樣 subscribeNext: 塊就可以安全執(zhí)行了。

很好,很簡(jiǎn)單!

構(gòu)建并運(yùn)行,看看頭像現(xiàn)在是否能正確顯示:

image

節(jié)流(Throtting)

你可能已經(jīng)注意到,每當(dāng)你輸入一個(gè)新的字符,Twitter 搜索就會(huì)立即執(zhí)行。如果你是一個(gè)快速打字的人(或者干脆按住刪除鍵),這可能會(huì)導(dǎo)致應(yīng)用程序一秒鐘執(zhí)行幾次搜索。這并不理想,原因有以下幾點(diǎn):首先,你在敲打 Twitter 搜索API,同時(shí)也丟掉了大部分搜索結(jié)果。第二,你在不斷地更新結(jié)果,這對(duì)用戶(hù)來(lái)說(shuō)是相當(dāng)分心的。

一個(gè)更好的方法是,只有在搜索文本在短時(shí)間內(nèi)沒(méi)有變化的情況下才執(zhí)行搜索,比如 500 毫秒。

你可能已經(jīng)猜到了,ReactiveCocoa 讓這個(gè)任務(wù)變得非常簡(jiǎn)單!

打開(kāi) RWSearchFormViewController.m,在 viewDidLoad 結(jié)尾處更新管道,在 filter 過(guò)濾器之后添加一個(gè) throttle 步驟:

[[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal * _Nonnull{
    @strongify(self)
    return self.searchText.rac_textSignal;
}] filter:^BOOL(id  _Nullable value) {
    @strongify(self)
    return [self isValidSearchText:value];
}] throttle:0.5]
   flattenMap:^__kindof RACSignal * _Nullable(id  _Nullable value) {
    @strongify(self)
    return [self signalForSearchWithText:value];
}] 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 * _Nullable error) {
    NSLog(@"An error occurred: %@", error);
}];

只有在給定的時(shí)間段內(nèi)沒(méi)有收到另一個(gè)下一個(gè)事件時(shí),節(jié)流操作才會(huì)發(fā)送下一個(gè)事件。真的就這么簡(jiǎn)單!

構(gòu)建并運(yùn)行確認(rèn),只有當(dāng)你停止輸入時(shí)長(zhǎng)超過(guò) 500 毫秒時(shí),搜索結(jié)果才會(huì)更新。感覺(jué)好多了不是嗎?你的用戶(hù)也會(huì)這么認(rèn)為。

而且......有了最后一步,你的 Twitter 即時(shí)應(yīng)用就完成了。給自己拍拍背,跳個(gè)快樂(lè)的舞蹈吧。

如果你在教程中的某個(gè)地方迷失了,你可以下載最終項(xiàng)目(別忘了在打開(kāi)之前從項(xiàng)目的目錄中運(yùn)行 pod install),或者你可以從GitHub上獲取代碼,在 GitHub 上,本教程中的每個(gè)構(gòu)建和運(yùn)行步驟都有一個(gè)提交。

總結(jié)

在出發(fā)享受一杯勝利咖啡之前,有必要欣賞一下最后的應(yīng)用程序流水線(xiàn):

image

這是相當(dāng)復(fù)雜的數(shù)據(jù)流,都簡(jiǎn)明扼要地表達(dá)為一條響應(yīng)式管道。這真是一道美麗的風(fēng)景線(xiàn)! 你能想象,如果使用非響應(yīng)式技術(shù),這個(gè)應(yīng)用會(huì)有多復(fù)雜嗎?而要看到這樣的應(yīng)用中的數(shù)據(jù)流又會(huì)有多難呢?聽(tīng)起來(lái)非常繁瑣,現(xiàn)在你再也不用走這條路了!

現(xiàn)在你知道 ReactiveCocoa 真的相當(dāng)厲害了吧!

最后一點(diǎn),ReactiveCocoa 使得使用 Model View ViewModel,也就是 MVVM 設(shè)計(jì)模式成為可能,它可以更好地分離應(yīng)用邏輯和視圖邏輯。如果有人對(duì)關(guān)于 MVVM 與 ReactiveCocoa 的后續(xù)文章感興趣,請(qǐng)?jiān)谠u(píng)論中告訴我。我很想聽(tīng)聽(tīng)你的想法和經(jīng)驗(yàn)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,327評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,554評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,883評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,114評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評(píng)論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,615評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容