ReactiveCocoa 教程-權威介紹/ 第1/2部分

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

對一名 iOS 開發(fā)者來說,你幾乎寫的每一行代碼都是對某些事件的響應夺颤;按鈕的點擊桩撮,接收到的網絡消息,屬性值的改變(通過 Key Value Observing)或者通過 CoreLocation 更新用戶的位置都是這方面很好的例子飒筑。然而,這些事件都是以不同的方式處理的胆萧,如動作绞呈、委托纱意、KVO婶溯、回調等等。ReactiveCocoa 為事件定義了一套標準的接口妇穴,因此可以使用一套基本的工具更容易地對它們進行鏈接爬虱、過濾和組合。

聽起來令人困惑腾它?神往跑筝?......心驚肉跳?那么請繼續(xù)閱讀 :]

ReactiveCocoa 結合了幾種編程風格:

  • 函數式編程(Functional Programming)其中使用了高階函數瞒滴,即以其他函數為參數的函數曲梗。
  • 響應式編程(Reactive Programming)它的重點是數據流和變化的傳遞。

出于這個原因妓忍,你可能會聽到 ReactiveCocoa 被描述為一個函數響應式編程(Functional Reactive Programming虏两,FRP)框架。

請放心世剖,這就是本教程所有學術性的內容定罢。編程范式是一門引人入勝的學科,但本 ReactiveCocoa 教程的剩余部分只關注實用價值旁瘫,通過代碼實例描述它的工作方式而不是理論學術知識祖凫。

Reactive 游樂園

在整個 ReactiveCocoa 教程中,你將會把響應式編程添加到一個非常簡單的示例應用程序中酬凳,即 ReactivePlayground惠况。下載初始化項目,然后編譯并運行以驗證你是否正確設置了一切宁仔。

ReactivePlayground 是一款非常簡單的應用稠屠,它向用戶呈現了一個登錄界面。輸入正確的憑證翎苫,想象一下权埠,用戶名是 user,密碼是 password煎谍,然后你會看到一張可愛的小貓咪的圖片弊知。

image

啊! 真可愛!

現在粱快,花點時間看看這個入門項目中的代碼是個不錯的起點。它很簡單,所以應該不會花很長時間事哭。

打開 RWViewController.m 文件漫雷,看看你能以多快的速度識別出 Sign In 按鈕啟用的條件?顯示或者隱藏 signInFailure Label 的規(guī)則又是什么鳍咱?在這個相對簡單的例子中降盹,回答這些問題可能只需要一兩分鐘。然而在更復雜的例子中谤辜,通過同樣類型的分析查清楚這些規(guī)則可能需要更長的時間蓄坏。

使用 ReactiveCocoa 之后,應用程序的底層意圖就會變得更加清晰丑念。是時候開始了!

添加 ReactiveCocoa 框架

注:ReactiveCocoa 框架現在已經更新并劃分為 ReactiveObjcReactiveCocoa

  • ReactiveObjc 對應的是 RAC 的 Objective-C 語言版本涡戳,最新的是 3.1.1 版本。
  • ReactiveCocoa 對應的是 RAC 的 Swift 語言版本脯倚,最新的是 11.1.0 版本渔彰。

以下所提及的 ReactiveCocoa 在本文中指的其實就是 ReactiveObjc。

因為本教程基于 Objective-C 語言推正,所以我們應該使用 ReactiveObjc 框架恍涂。

將 ReactiveObjc 框架添加到項目中最簡單的方法是使用 CocoaPods。如果你從來沒有使用過 CocoaPods植榕,遵循本網站上的 CocoaPod介紹 教程可能是有意義的再沧,或者至少運行該教程的初始步驟,以便你可以安裝本教程的先決條件尊残。

注意:如果出于某些原因你不想使用 CocoaPods炒瘸,你仍然可以使用 ReactiveObjc,只要按照 GitHub 上文檔中的 導入ReactiveObjc 的步驟即可夜郁。

如果你現在仍然在 Xcode 中打開著 ReactivePlayground 項目什燕,那么現在就關閉它。CocoaPods 會創(chuàng)建一個 Xcode 工作空間竞端,你會用它來代替原來的項目文件屎即。

打開 Terminal 終端。將路徑導航到你項目所在的文件夾事富,然后輸入以下內容:

pod init
vim Podfile

這里會創(chuàng)建一個名為 Podfile 的初始化文件技俐,并用 vim 打開它。將 pod 'ReactiveObjC', '~> 3.1.1' 添加到你的 Podfile 文件中:

# 指明依賴庫的來源地址统台,不使用默認 CDN
source 'https://github.com/CocoaPods/Specs.git'

# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

target 'RWReactivePlayground' do
  # Comment the next line if you don't want to use dynamic frameworks
  # use_frameworks!

  # Pods for RWReactivePlayground
  pod 'ReactiveObjC', '~> 3.1.1'

  target 'RWReactivePlaygroundTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

這里將平臺設置為 iOS雕擂,SDK 最低版本為 9.0,并將 ReactiveObjC 框架添加為依賴關系贱勃。

保存好這個文件后井赌,回到 Terminal 窗口谤逼,輸入以下命令:

pod install

你應該看到一個類似于下面的輸出:

Analyzing dependencies
Downloading dependencies
Installing ReactiveObjC (3.1.1)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `RWReactivePlayground.xcworkspace` for this project from now on.
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

這說明 ReactiveObjC 框架已經下載完畢,CocoaPods 已經創(chuàng)建了一個 Xcode workspace仇穗,并將框架集成到了你現有的應用中流部。

打開新生成的工作空間,RWReactivePlayground.xcworkspace纹坐,看看 CocoaPods 在項目導航欄里面創(chuàng)建的結構:

image

你應該看到 CocoaPods 創(chuàng)建了一個新的工作空間枝冀,并添加了原始項目 RWReactivePlayground,以及一個包含 ReactiveObjc 的 Pods 項目耘子。CocoaPods 確實讓管理依賴關系變得輕而易舉!

你會注意到這個項目的名字叫 ReactivePlayground果漾,所以這一定意味著是時候玩了......。

是時候玩了

正如前言中所提到的谷誓,ReactiveCocoa 提供了一個標準的接口來處理應用程序中發(fā)生的不同事件流绒障。在 ReactiveCocoa 術語中,這些事件被稱為信號(signal)片林,并由 RACSignal 類來表示端盆。

打開本應用的初始視圖控制器 RWViewController.m,在文件頂部添加以下內容费封,導入 ReactiveObjc 頭文件:

#import <ReactiveObjC/ReactiveObjC.h>

你還不打算替換任何現有代碼焕妙,現在你只是要玩會兒。在 viewDidLoad 方法的末尾添加以下代碼:

// self 訂閱 usernameTextField 的 text 信號弓摘,接收 next 事件
[self.usernameTextField.rac_textSignal subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"%@", x);
}];

編譯并運行應用程序焚鹊,并在用戶名輸入框中輸入一些文本。注意觀察控制臺韧献,看看是否有類似下面的輸出:

2020-12-25 10:42:36.037050+0800 RWReactivePlayground[24251:2953713] i
2020-12-25 10:42:36.715071+0800 RWReactivePlayground[24251:2953713] is
2020-12-25 10:42:38.627540+0800 RWReactivePlayground[24251:2953713] is
2020-12-25 10:42:39.327718+0800 RWReactivePlayground[24251:2953713] is t
2020-12-25 10:42:39.743444+0800 RWReactivePlayground[24251:2953713] is th
2020-12-25 10:42:40.213227+0800 RWReactivePlayground[24251:2953713] is thi
2020-12-25 10:42:40.382229+0800 RWReactivePlayground[24251:2953713] is this
2020-12-25 10:42:40.702870+0800 RWReactivePlayground[24251:2953713] is this
2020-12-25 10:42:41.430766+0800 RWReactivePlayground[24251:2953713] is this m
2020-12-25 10:42:41.573035+0800 RWReactivePlayground[24251:2953713] is this ma
2020-12-25 10:42:42.733458+0800 RWReactivePlayground[24251:2953713] is this mag
2020-12-25 10:42:42.886624+0800 RWReactivePlayground[24251:2953713] is this magi
2020-12-25 10:42:43.047428+0800 RWReactivePlayground[24251:2953713] is this magic
2020-12-25 10:42:44.066096+0800 RWReactivePlayground[24251:2953713] is this magic?

你可以看到末患,每當你在用戶名輸入框中更改文本時,Block 塊中的代碼就會執(zhí)行锤窑。沒有 target-action璧针,沒有 delegate 委托察净,只有信號和Block 塊葵擎。這太令人興奮了!

ReactiveCocoa 信號(用 RACSignal 表示)向其訂閱者發(fā)送事件流蛮放。有三種類型的事件需要知道:next绘证、errorcompleted隧膏。一個信號在出錯并終止或者完成之前,可以發(fā)送任意數量的 next 事件嚷那。在本教程中胞枕,將重點介紹 next 事件。如果要開始了解 errorcompleted 事件魏宽,請務必閱讀本教程的第二部分腐泻。

RACSignal 有許多方法可以用來訂閱不同的事件類型决乎。每個方法都需要一個或多個 Block 塊,當一個事件發(fā)生時贫悄,Block 塊中的邏輯就會被執(zhí)行瑞驱。在本例中,你可以看到 subscribeNext: 方法提供了一個 Block 塊窄坦,用來觸發(fā)并執(zhí)行每一次的 next 事件。

ReactiveCocoa 框架使用 categories 來為許多標準的 UIKit 控件添加信號凳寺,這樣你就可以為它們的事件添加訂閱鸭津,這就是 UITextField 上 rac_textSignal 屬性的由來。

但理論上的東西已經夠多了肠缨,是時候開始讓 ReactiveCocoa 替你干活了逆趋。

ReactiveCocoa 有大量的操作符讓你用來操作事件流。例如晒奕,假設你只對長度超過三個字符的用戶名感興趣闻书。你可以通過使用 filter 操作符來實現。將之前在 viewDidLoad 中添加的代碼更新為以下內容:

[[self.usernameTextField.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
    return value.length > 3;
    }] subscribeNext:^(NSString * _Nullable x) {
        NSLog(@"%@", x);
    }];

如果你編譯并運行脑慧,然后在用戶名輸入框中輸入一些文本魄眉,你應該會發(fā)現,只有當文本字段長度大于三個字符時闷袒,它才開始記錄:

2020-12-25 11:01:58.183761+0800 RWReactivePlayground[24588:2970617] is t
2020-12-25 11:01:58.349105+0800 RWReactivePlayground[24588:2970617] is th
2020-12-25 11:01:58.597578+0800 RWReactivePlayground[24588:2970617] is thi
2020-12-25 11:01:58.665779+0800 RWReactivePlayground[24588:2970617] is this
2020-12-25 11:02:00.711772+0800 RWReactivePlayground[24588:2970617] is this
2020-12-25 11:02:01.071901+0800 RWReactivePlayground[24588:2970617] is this m
2020-12-25 11:02:01.182999+0800 RWReactivePlayground[24588:2970617] is this ma
2020-12-25 11:02:01.387688+0800 RWReactivePlayground[24588:2970617] is this mag
2020-12-25 11:02:02.913319+0800 RWReactivePlayground[24588:2970617] is this magi
2020-12-25 11:02:03.058652+0800 RWReactivePlayground[24588:2970617] is this magic
2020-12-25 11:02:03.651991+0800 RWReactivePlayground[24588:2970617] is this magic?

你在這里創(chuàng)建的是一個非常簡單的管道(pipeline)坑律。它是響應式編程的精髓,通過數據流來表達應用程序的功能囊骤。

我們可以用數據流圖的方式來描述它:

image

上圖中晃择,你可以看到 rac_textSignal 是事件的初始來源。數據流經一個 filter 過濾器也物,只有當事件包含一個長度大于 3 的字符串時宫屠,才允許事件通過。管道的最后一步是通過 subscribeNext: 方法中的 Block 塊記錄事件值滑蚯。

值得注意的是浪蹂,filter 過濾器操作的輸出也是一個 RACSignal。你可以將代碼安排如下膘魄,以顯示離散的管道步驟:

RACSignal *usernameSourceSignal = self.usernameTextField.rac_textSignal;

RACSignal *filteredUsername = [usernameSourceSignal filter:^BOOL(id  _Nullable value) {
    NSString *text = value;
    return text.length > 3;
}];

[filteredUsername subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

因為對 RACSignal 的每一步操作都會返回一個 RACSignal乌逐,所以被稱為 Fluent interface。這個特性允許你構建管道创葡,而不需要使用局部變量來引用每個步驟浙踢。

注意:ReactiveCocoa 大量使用了 Blocks。如果你是 block 編程的新手灿渴,你可能會想閱讀 Apple 的 Blocks 編程主題洛波。如果你和我一樣胰舆,熟悉 Block,但發(fā)現語法有點混亂和難以記憶蹬挤,你可能會發(fā)現標題有趣的 f******gblocksyntax.com/ 相當有用缚窿! (為了保護無辜者,我們刪掉了這個詞焰扳,但鏈接是完全可以使用的倦零。)

一點點鑄造

如果你更新了代碼,把它分割成各種 RACSignal 組件吨悍,現在是時候把它恢復到流暢的語法了:

[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) {
    NSString *text = value; // 隱式轉換
    return text.length > 3;
    }] subscribeNext:^(NSString * _Nullable x) {
        NSLog(@"%@", x);
    }];

上面代碼中扫茅,從 idNSString 的隱式轉換并不優(yōu)雅。幸運的是育瓜,由于傳遞到這個 Block 塊的值總是 NSString 類型葫隙,因此你可以改變參數類型本身。更新你的代碼如下:

[[self.usernameTextField.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
    return value.length > 3;
    }] subscribeNext:^(NSString * _Nullable x) {
        NSLog(@"%@", x);
    }];

編譯并運行躏仇,確認這和之前一樣工作恋脚。

什么是事件?

到目前為止焰手,本教程已經描述了不同的事件類型糟描,但還沒有詳細介紹這些事件的結構。有趣的是册倒,一個事件絕對可以包含任何東西!

作為對這點的說明蚓挤,你將在管道中添加另一個操作。更新你添加到 viewDidLoad 的代碼如下:

[[[self.usernameTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
        // map 方法:通過 Block 塊對事件中的數據進行轉換
        // 此 Block 塊中驻子,接受 NSString 類型的輸入并獲取字符串的長度灿意,返回一個 NSNumber 類型
        return @(value.length);
    }] filter:^BOOL(NSNumber *length) {
        return length.integerValue > 3;
    }] subscribeNext:^(id  _Nullable x) {
        NSLog(@"%@", x);
    }];

如果你編譯并運行,你會發(fā)現應用程序現在記錄的是文本的長度崇呵,而不是內容:

2020-12-25 11:53:22.507776+0800 RWReactivePlayground[25492:3015912] 4
2020-12-25 11:53:22.619187+0800 RWReactivePlayground[25492:3015912] 5
2020-12-25 11:53:22.849555+0800 RWReactivePlayground[25492:3015912] 6
2020-12-25 11:53:22.930413+0800 RWReactivePlayground[25492:3015912] 7
2020-12-25 11:53:23.410323+0800 RWReactivePlayground[25492:3015912] 8
2020-12-25 11:53:23.682407+0800 RWReactivePlayground[25492:3015912] 9
2020-12-25 11:53:23.759639+0800 RWReactivePlayground[25492:3015912] 10
2020-12-25 11:53:23.975977+0800 RWReactivePlayground[25492:3015912] 11
2020-12-25 11:53:25.152581+0800 RWReactivePlayground[25492:3015912] 12

新增加的 map 操作通過 Block 塊對事件中的數據進行了轉換缤剧。每接收到一個 next 事件,它都會運行給定的 Block 塊域慷,并將返回值作為 next event 發(fā)出荒辕。在上面的代碼中,map 接收NSString 類型的輸入并獲取其長度值犹褒,并返回一個 NSNumber 類型抵窒。

如果想了解其令人驚嘆的工作原理,請看這張圖:

image

正如你所看到的叠骑,所有在 map 操作之后的步驟現在都會接收到 NSNumber 實例李皇。你可以使用 map 操作將接收到的數據轉化為任何你喜歡的東西,只要它是一個對象宙枷。

注意:在上面的示例中掉房,text.length 屬性返回一個 NSUInteger 類型茧跋,這是一個基礎數據類型(基礎數據類型不是對象)。為了將它作為事件的內容使用卓囚,它必須被裝箱瘾杭。幸運的是,Objective-C 的字面量語法提供了一個相當簡潔的方式來實現這一點--@(text.length)哪亿。

玩夠了! 現在是時候更新 ReactivePlayground 應用程序了粥烁,并使用到目前為止你所學到的概念。你可以刪除你在本教程開始部分添加的所有代碼锣夹。

創(chuàng)建驗證狀態(tài)的信號

首先页徐,你需要做的是創(chuàng)建幾個信號來驗證用戶名和密碼輸入框中輸入的內容是否有效。在 RWViewController.m 中的 viewDidLoad 末尾添加以下內容:

RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
    return @([self isValidUsername:value]);
}];

RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
    return @([self isValidPassword:value]);
}];

正如你所看到的银萍,上面的代碼對每個文本輸入框的 rac_textSignal 進行了 map 變換。輸出的是一個通過 NSNumber 封裝的布爾值恤左。

下一步是轉換這些信號贴唇,使它們?yōu)槲谋据斎肟蛱峁┮粋€漂亮的背景色。通常飞袋,你訂閱這個信號戳气,并使用結果來更新文本輸入框的背景色。一個可行的方案如下:

[[validPasswordSignal map:^id _Nullable(NSNumber *passwordValid) {
        return passwordValid.boolValue ? UIColor.clearColor : UIColor.yellowColor;
    }] subscribeNext:^(UIColor *color) {
        self.passwordTextField.backgroundColor = color;
    }];

請不要添加這段代碼巧鸭,還有一個更優(yōu)雅的解決方案瓶您。

從概念上講,你把這個信號的輸出分配給文本輸入框的 backgroundColor 屬性纲仍。然而呀袱,上面的代碼是一個很差的表達方式,都是倒過來的郑叠。

幸運的是夜赵,ReactiveCocoa 有一個宏,可以讓你優(yōu)雅地表達這一點乡革。在你添加到 viewDidLoad 的兩個信號下面直接添加下面的代碼:

RAC(self.usernameTextField, backgroundColor) = [validUsernameSignal map:^id _Nullable(NSNumber *usernameValid) {
    return usernameValid.boolValue ? UIColor.clearColor : UIColor.yellowColor;
}];

RAC(self.passwordTextField, backgroundColor) = [validPasswordSignal map:^id _Nullable(NSNumber *passwordValid) {
    return passwordValid.boolValue ? UIColor.clearColor : UIColor.yellowColor;
}];

RAC 宏允許你將信號的輸出分配給對象的屬性寇僧。它需要兩個參數,第一個是包含要設置屬性的對象沸版,第二個是對象的屬性名嘁傀。每次信號發(fā)出 next 事件時,傳遞的值都會被分配給給定的屬性视粮。

這是一個非常優(yōu)雅的解決方案细办,你不覺得嗎?

在編譯和運行之前馒铃,還有最后一件事蟹腾。找到 updateUIState 方法痕惋,刪除前兩行:

self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];

這里刪除了非響應式(non-reactive)代碼。

編譯并運行應用程序娃殖。你應該發(fā)現值戳,文本輸入框字段在無效時看起來是高亮狀態(tài),有效時則清除高亮狀態(tài)炉爆。

可視化很有用堕虹,所以這里有一種方法來可視化當前的邏輯。在這里你可以看到兩個簡單的管道芬首,它們接收文本信號赴捞,將其映射到描述有效性的布爾值,然后跟著第二個映射到 UIColor郁稍,這是與文本字段的背景顏色綁定的部分赦政。

image

你是否想知道為什么要創(chuàng)建單獨的 validPasswordSignalvalidUsernameSignal 信號,而不是為每個文本字段創(chuàng)建一個單一的 fluent 管道耀怜?親愛的讀者恢着,請耐心等待,這個瘋狂背后的方法很快就會變得清晰起來!

組合信號

在當前應用程序中财破,只有當用戶名和密碼輸入框字段都是有效輸入時掰派,登錄按鈕才會工作。現在是時候用響應式風格來實現了左痢。

當前的代碼中已經有信號發(fā)出布爾值來描述用戶名和密碼字段是否有效靡羡;即 validUsernameSignalvalidPasswordSignal。你的任務是結合這兩個信號來決定何時可以啟用按鈕俊性。

viewDidLoad 的末尾添加以下內容:

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

上面的代碼使用 combineLatest:reduce: 方法將 validUsernameSignalvalidPasswordSignal 發(fā)出的最新值合并成一個閃亮的新信號略步。每當兩個源信號中的任何一個發(fā)出新的值時,reduce 塊就會執(zhí)行磅废,它返回的值作為合并信號的下一個值發(fā)送纳像。

注意RACSignalcombine 方法可以組合任意數量的信號,reduce 塊的參數對應于每個源信號拯勉。ReactiveCocoa 有一個狡猾的小實用類 RACBlockTrampoline竟趾,它在內部處理 reduce 塊的變量參數列表。事實上宫峦,ReactiveCocoa 的實現中隱藏著很多狡猾的技巧岔帽,所以很值得拉開蓋子!

現在你有了一個合適的信號,在 viewDidLoad 的結尾添加以下內容导绷。這將把它連接到按鈕的 enabled 屬性:

[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
    self.signInButton.enabled = signupActive.boolValue;
}];

在運行這段代碼之前犀勒,是時候移除舊的實現了。從文件頂部刪除這兩個屬性:

@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;

viewDidLoad 的頂部,刪除以下內容:

// 處理兩個文本輸入框的輸入內容更新
[self.usernameTextField addTarget:self action:@selector(usernameTextFieldChanged) forControlEvents:UIControlEventEditingChanged];
[self.passwordTextField addTarget:self action:@selector(passwordTextFieldChanged) forControlEvents:UIControlEventEditingChanged];

同時刪除 updateUIState贾费、usernameTextFieldChangedpasswordTextFieldChanged 方法钦购。呼!你剛剛處理掉了很多非響應式代碼褂萧。你會感謝你所做的押桃。

最后,確保從 viewDidLoad 中刪除對 updateUIState 方法的調用导犹。

如果你編譯并運行唱凯,檢查登錄按鈕。它應該被啟用谎痢,因為用戶名和密碼文本字段是有效的磕昼,就像之前一樣。

對應用邏輯圖進行更新后节猿,得到以下內容:

image

上面闡述了幾個重要的概念票从,這些概念允許你用 ReactiveCocoa 執(zhí)行一些非常強大的任務。

  • Splitting 拆分——信號可以有多個訂閱者滨嘱,并作為多個后續(xù)管道步驟的輸入源纫骑。在上圖中,請注意表示密碼和用戶名有效性的布爾信號被拆分并用于幾個不同的目的九孩。
  • Combining 合并——多個信號可以被合并以創(chuàng)建新的信號。在這種情況下发框,兩個布爾信號被組合起來躺彬。然而,你可以組合發(fā)出任何值類型的信號梅惯。

這些變化的結果就是宪拥,應用程序不再具有描述兩個文本輸入框字段當前有效狀態(tài)的私有屬性。這是采用響應式風格時你會發(fā)現的關鍵區(qū)別之一——你不需要使用實例變量來跟蹤瞬時狀態(tài)铣减。

Reactive 登錄

目前她君,該應用程序使用上面說明的響應式管道來管理文本輸入框和按鈕的狀態(tài)。然而葫哗,按鈕的點擊處理仍然使用 action 動作缔刹,所以下一步是替換剩下的應用邏輯,以便使其全部成為響應式的劣针!

登錄按鈕上的 Touch Up Inside 事件通過 storyboard 上的 action 連接到 RWViewController.m 中的 signInButtonTouched 方法上校镐。你要用響應式等價物來代替它,所以你首先需要斷開當前的 storyboard 動作捺典。

打開 Main.storyboard鸟廓,找到 Sign In 按鈕,ctrl 點擊調出 outlet/action 連接,點擊 x 刪除連接引谜。如果你感到茫然牍陌,下圖貼心地告訴你在哪里可以找到刪除按鈕:

image

你已經看到了 ReactiveCocoa 框架是如何為標準的 UIKit 控件添加屬性和方法的。到目前為止员咽,你已經使用了rac_textSignal毒涧,它在文本輸入框內容變化時發(fā)出事件。為了處理按鈕點擊事件骏融,你需要使用 ReactiveCocoa 添加到 UIKit 上的另一個方法链嘀,rac_signalForControlEvents

返回 RWViewController.m档玻,在 viewDidLoad 的末尾添加以下內容:

[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"button clicked");
}];

上面的代碼從按鈕的 UIControlEventTouchUpInside 事件中創(chuàng)建了一個信號怀泊,并添加了一個訂閱,以便在每次這個事件發(fā)生時執(zhí)行一次日志記錄误趴。

編譯并運行以確認該消息確實記錄下來霹琼。請記住,只有當用戶名和密碼有效時凉当,按鈕才會啟用枣申,所以在點擊按鈕之前,一定要在這兩個字段中輸入一些文本看杭!

你應該在 Xcode 控制臺中看到類似以下的消息:

2020-12-25 14:17:08.643189+0800 RWReactivePlayground[27851:3123372] button clicked
2020-12-25 14:17:09.235768+0800 RWReactivePlayground[27851:3123372] button clicked
2020-12-25 14:17:10.550770+0800 RWReactivePlayground[27851:3123372] button clicked
2020-12-25 14:17:10.909417+0800 RWReactivePlayground[27851:3123372] button clicked
2020-12-25 14:17:11.312301+0800 RWReactivePlayground[27851:3123372] button clicked

現在按鈕已經有了一個觸摸事件的信號忠藤,下一步就是把這個與登錄過程本身連接起來。出現了一個問題--不過這很好楼雹,你不介意出現問題吧模孩?打開 RWDummySignInService.h,看一下界面贮缅。

#import <Foundation/Foundation.h>

typedef void (^RWSignInResponse)(BOOL);

@interface RWDummySignInService : NSObject

- (void)signInWithUsername:(NSString *)username password:(NSString *)password complete:(RWSignInResponse)completeBlock;

@end

該服務將用戶名榨咐、密碼和完成 Block 塊作為參數。當登錄成功或失敗時谴供,給定的 Block 塊將被執(zhí)行块茁。你可以直接在當前記錄按鈕觸摸事件的 subscribeNext: 塊中使用這個接口,但你為什么要這樣做呢桂肌?這就是 ReactiveCocoa 當早餐吃的那種異步的数焊、基于事件的行為!

注意:本教程中為了簡單起見,使用了一個虛擬服務轴或,這樣你就不會對外部 API 產生任何依賴昌跌。然而,你現在遇到了一個很現實的問題照雁,如何使用不是用信號表達的 API蚕愤?

創(chuàng)建信號

幸運的是答恶,將現有的異步 API 改為信號表示相當容易。首先萍诱,從 RWViewController.m 中刪除當前的 signInButtonTouched: 方法悬嗓,你不需要這個邏輯,因為它將被一個響應式的等價物取代裕坊。

留在 RWViewController.m 中包竹,添加以下方法:

- (RACSignal *)signInSingal {
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        [self.signInService signInWithUsername:self.usernameTextField.text password:self.passwordTextField.text complete:^(BOOL success) {
            [subscriber sendNext:@(success)];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
}

上述方法創(chuàng)建了一個用當前用戶名和密碼登錄的信號。現在來分析一下它的組件部分籍凝。

上面的代碼使用 RACSignal 上的 createSignal: 方法來創(chuàng)建信號周瞎。描述這個信號并傳遞給這個方法的 Block 塊是一個單一的參數。當這個信號有一個訂閱者時饵蒂,Block 塊內的代碼就會執(zhí)行声诸。

該 Block 塊被傳遞給一個單一的 subscriber 訂閱者實例,該實例遵守 RACSubscriber 協議退盯,它的方法是你為了發(fā)出事件而調用的彼乌;你也可以發(fā)送任意數量的 next 事件,以 errorcomplete 事件結束渊迁。在這種情況下慰照,它發(fā)送一個單一的 next 事件來指示登錄是否成功,然后是一個 complete 事件琉朽。

這個 Block 塊的返回類型是一個 RACDisposable 對象毒租,它允許你在取消或銷毀訂閱時執(zhí)行任何可能需要的清理工作。這個信號沒有任何清理要求箱叁,因此返回 nil蝌衔。

正如你所看到的,將一個異步 API 包裹在一個信號中是非常簡單的蝌蹂!

現在來使用這個新信號。更新你在上一節(jié)中添加到 viewDidLoad 結尾的代碼曹锨,如下所示:

// 外層是一個按鈕觸摸事件的信號
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id _Nullable(__kindof UIControl * _Nullable value) {
        // 內層創(chuàng)建并返回了一個登錄事件信號
        return [self signInSingal];
    }] subscribeNext:^(id  _Nullable x) {
        NSLog(@"Sign in result: %@", x);
    }];

上面的代碼使用前面的 map 方法將按鈕觸摸信號轉化為登錄信號孤个。用戶只需將結果記錄下來即可。

如果你編譯并運行應用沛简,然后點擊登錄按鈕齐鲤,再看看 Xcode 控制臺,你會看到上面代碼的結果......椒楣。

...... 結果并不像你想象的那樣!

2020-12-25 14:39:37.575620+0800 RWReactivePlayground[28255:3143972] Sign in result: <RACDynamicSignal: 0x600001286e20> name:

沒錯给郊, subscribeNext: 塊已經被傳遞了一個信號,但并不是登錄信號返回的結果!

是時候說明這個管道了捧灰,這樣你就可以看到發(fā)生了什么:

image

當你點擊按鈕時淆九,rac_signalForControlEvents 會發(fā)出 next 事件(以 UIButton 作為其事件數據源,也就是說,這是一個按鈕點擊信號的事件)炭庙。map 步驟創(chuàng)建并返回登錄信號饲窿,這意味著下面的管道步驟現在收到了一個 RACSignal。這就是你在 subscribeNext: 步驟中觀察到的情況焕蹄。

注釋:按鈕點擊信號的 next 事件返回了登錄信號本身逾雄,而不是返回登錄信號包含的事件內容。

上面的情況有時被稱為信號的信號腻脏,換句話說鸦泳,一個外在的信號,包含一個內在的信號永品。如果你真的想這樣做做鹰,你可以在外部信號的 subscribeNext: 塊中訂閱內部信號。然而這將導致一個嵌套的混亂! 幸運的是腐碱,這是一個常見的問題誊垢,ReactiveCocoa 已經為這種情況做好了準備。

信號的信號

這個問題的解決方法很簡單症见,只要將 map 步驟改為 flattenMap 步驟喂走,如下所示:

// 通過 flattenMap 方法將按鈕點擊信號的事件轉換為登錄信號的事件
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id _Nullable(__kindof UIControl * _Nullable value) {
        return [self signInSingal];
    }] subscribeNext:^(id  _Nullable x) {
        NSLog(@"Sign in result: %@", x);
    }];

這樣就可以像以前一樣,把按鈕點擊信號的事件 map 到登錄信號上谋作,但同時也將事件從內側信號發(fā)送到外側信號上芋肠,使之扁平化。

編譯并運行遵蚜,并關注控制臺√兀現在它應該會記錄登錄是否成功:

2020-12-25 14:52:41.374075+0800 RWReactivePlayground[28486:3156910] Sign in result: 1
2020-12-25 14:52:45.881490+0800 RWReactivePlayground[28486:3156910] Sign in result: 1

令人激動!

現在吭净,管道正在做你想要的事情睡汹,最后一步是為 subscribeNext 步驟添加邏輯,以便在成功登錄后執(zhí)行所需的導航寂殉。將管道替換為以下內容:

[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id _Nullable(__kindof UIControl * _Nullable value) {
        return [self signInSingal];
    }] subscribeNext:^(NSNumber *signIn) {
        BOOL success = signIn.boolValue;
        self.signInFailureText.hidden = success;
        if (success) {
            [self performSegueWithIdentifier:@"signInSuccess" sender:self];
        }
    }];

subscribeNext: 塊從登錄信號中獲取結果囚巴,相應地更新 signInFailureText 文本字段的可見性,并在需要時執(zhí)行導航切換友扰。

編譯并運行彤叉,再去享受一次小貓的樂趣吧! 喵!

image

你是否注意到當前應用有一個小小的用戶體驗問題?當登錄服務正在驗證提供的憑證時村怪,應該禁用登錄按鈕秽浇。這可以防止用戶重復進行相同的登錄。此外甚负,如果發(fā)生了一次失敗的登錄嘗試柬焕,當用戶再次嘗試登錄時审残,應該隱藏錯誤信息。

但是應該如何將這個邏輯添加到當前的管道中呢击喂?改變按鈕的啟用狀態(tài)并不是一個轉換维苔、過濾器或任何其他你迄今為止遇到的概念。相反懂昂,它是所謂的 side-effect;介时;或者你想在下一個事件發(fā)生時在管道內執(zhí)行的邏輯,但它實際上并沒有改變事件本身的性質凌彬。

添加 side-effect

將目前的管道改為:

[[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside]
   doNext:^(__kindof UIControl *_Nullable x) {
       self.signInButton.enabled = NO;
       self.signInFailureText.hidden = YES;
   }] flattenMap:^id _Nullable (__kindof UIControl *_Nullable value) {
       return [self signInSingal];
   }] subscribeNext:^(NSNumber *signIn) {
       self.signInButton.enabled = YES;
       BOOL success = signIn.boolValue;
       self.signInFailureText.hidden = success;
       if (success) {
           [self performSegueWithIdentifier:@"signInSuccess" sender:self];
       }
   }];

你可以看到上面是如何在按鈕觸摸事件創(chuàng)建后立即向管道中添加 doNext: 步驟的沸柔。請注意,doNext: 塊并沒有返回一個值铲敛,因為它是一個副作用褐澎;它讓事件本身保持不變。

上面的 doNext: 塊將按鈕的啟用屬性設置為 NO伐蒋,并隱藏了失敗文本工三。而 subscribeNext: 塊則重新啟用按鈕,并根據登錄結果顯示或隱藏失敗文本先鱼。

現在是時候更新管道流圖以包含這個副作用了俭正。沐浴在它的光輝之中吧:

image

編譯并運行應用程序,以確認登錄按鈕按照預期的方式啟用和禁用焙畔。

這樣掸读,你的工作就完成了--應用程序現在已經完全實現了響應式編程。Woot!

如果你在中途迷路了宏多,你可以下載最終項目(包括完整的依賴關系)儿惫,或者你可以從 GitHub 上獲取代碼,在 GitHub上的 commit 提交歷史中伸但,可以匹配本教程中的每個構建和運行步驟肾请。

注意:在一些異步活動進行時禁用按鈕是一個常見的問題,ReactiveCocoa 再次對這個小問題進行了處理更胖。RACCommand 封裝了這個概念筐喳,并且有一個啟用信號,允許你將按鈕的啟用屬性連接到信號上函喉。你可能想試試這個類。

總結

希望本教程能給你打下一個良好的基礎荣月,當你開始在自己的應用程序中使用 ReactiveCocoa 時管呵,會對你有所幫助。習慣這些概念可能需要一點練習哺窄,但就像任何語言或程序一樣捐下,一旦你掌握了它的竅門账锹,它就會變得非常簡單。ReactiveCocoa 的核心是信號坷襟,它只不過是事件流奸柬。還有什么比這更簡單的呢?

在 ReactiveCocoa 中婴程,我發(fā)現了一個有趣的事情廓奕,那就是有很多方法可以解決同一個問題。你可能會想嘗試這個應用程序档叔,并調整信號和管道來改變它們的分割和組合方式桌粉。

值得考慮的是,ReactiveCocoa 的主要目標是讓你的代碼更干凈衙四,更容易理解铃肯。就我個人而言,我發(fā)現如果一個應用程序的邏輯被表示為清晰的管道传蹈,使用流暢的語法押逼,那么就更容易理解它的工作方式。

在本系列教程的第二部分惦界,您將學習更高級的主題挑格,如錯誤處理以及如何管理在不同線程上執(zhí)行的代碼。在此之前表锻,祝你實驗愉快!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末恕齐,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子瞬逊,更是在濱河造成了極大的恐慌显歧,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件确镊,死亡現場離奇詭異士骤,居然都是意外死亡,警方通過查閱死者的電腦和手機蕾域,發(fā)現死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門拷肌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人旨巷,你說我怎么就攤上這事巨缘。” “怎么了采呐?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵若锁,是天一觀的道長。 經常有香客問我斧吐,道長又固,這世上最難降的妖魔是什么仲器? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮仰冠,結果婚禮上桐智,老公的妹妹穿的比我還像新娘歉甚。我一直安慰自己叮趴,他們只是感情好贬养,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著木张,像睡著了一般众辨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舷礼,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天鹃彻,我揣著相機與錄音,去河邊找鬼妻献。 笑死蛛株,一個胖子當著我的面吹牛,可吹牛的內容都是我干的育拨。 我是一名探鬼主播谨履,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熬丧!你這毒婦竟也來了笋粟?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤析蝴,失蹤者是張志新(化名)和其女友劉穎害捕,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體闷畸,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡尝盼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了佑菩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盾沫。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖殿漠,靈堂內的尸體忽然破棺而出赴精,到底是詐尸還是另有隱情,我是刑警寧澤绞幌,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布蕾哟,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏渐苏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一菇夸、第九天 我趴在偏房一處隱蔽的房頂上張望琼富。 院中可真熱鬧,春花似錦庄新、人聲如沸鞠眉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽械蹋。三九已至,卻和暖如春羞芍,著一層夾襖步出監(jiān)牢的瞬間哗戈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工荷科, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留唯咬,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓畏浆,卻偏偏與公主長得像胆胰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子刻获,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容