這周在學習RAC,找到一篇很好的文章,(摘錄下來,對部份內容進行必定的翻譯,感受翻譯要比簡單的讀,更須要字斟句酌,更容易留下較深的印象,加深理解,翻譯出來,也能夠幫助到別人)。本來計劃是部分翻譯,最後沒有掌握好,仍是所有翻譯了,一共花費了將近十個小時,以前對這篇文章的研究也花費了將近十個小時,這樣效率有點低,下一步,要看看怎麼提升一下這個效率。html
這篇文章跟着走一遍,對於RAC基本就能有必定的印象了,而後再針對每一個細節來進行學習,就能較快掌握住RAC了,不然,只看官網,官網的文章寫得感受並非很清楚。react
原帖位置:https://www.raywenderlich.com/2493-reactivecocoa-tutorial-the-definitive-introduction-part-1-2。ios
Get to grips with ReactiveCocoa in this 2-part tutorial series. Put the paradigms to one-side, and understand the practical value with work-through examples.git
在這兩部分指導系統掌握 ReactiveCocoa。先把範式放在一邊,先經過對這些例子的實踐來理解實際的意思。github
As an iOS developer, nearly every line of code you write is in reaction to some event; a button tap, a received network message, a property change (via Key Value Observing) or a change in user’s location via CoreLocation are all good examples. However, these events are all encoded in different ways; as actions, delegates, KVO, callbacks and others. ReactiveCocoa defines a standard interface for events, so they can be more easily chained, filtered and composed using a basic set of tools.objective-c
做爲一個iOS開發者,幾乎你編寫的每一行代碼都是一個對某種事件的響應;一個按鈕的點擊,一個網絡消息的接收,一個屬性的改變(經過Key Value Observing)或者經過 CoreLocation 的用戶位置的改變都是很好的例子。而後,這些事件被用不一樣的方式進行的編碼;被當作 action, delegates, KVO, callback 和其餘。ReactiveCocoa 定義了一個標準的事件界面,這樣他們能夠被更容易地鏈化,過濾和使用一個基本的工具集來編寫。express
Sound confusing? Intriguing? … Mind blowing? Then read on :]編程
聽上去有些困惑?有趣? ... 使人興奮?來繼續閱讀吧 :]瀏覽器
ReactiveCocoa combines a couple of programming styles:網絡
ReactiveCocoa 由一組編程風格組成:
- Functional Programming which makes use of higher order functions, i.e. functions which take other functions as their arguments
函數式編程 這使用了不少高階函數,例如函數使用其它函數做爲參數
- Reactive Programming which focuses of data-flows and change propagation
響應式編程 這聚焦於數據流和改變的傳播
For this reason, you might hear ReactiveCocoa described as a Functional Reactive Programming (or FRP) framework.
由於這個緣由,你可能聽到過把ReactiveCocoa描述成一個函數響應式框架。
Rest assured, that is as academic as this tutorial is going to get! Programming paradigms are a fascinating subject, but the rest of this ReactiveCocoa tutorials focuses solely on the practical value, with work-through examples instead of academic theories.
請放心,這就像本教程的學術內容同樣! 編程範式是一個引人入勝的主題,但ReactiveCocoa教程的其他部分僅關注實用價值,並經過實例來代替學術理論。
Throughout this ReactiveCocoa tutorial, you’ll be introducing reactive programming to a very simple example application, the ReactivePlayground. Download the starter project, then build and run to verify you have everything set up correctly.
經過這個指導,你將引入響應式編程到一個很是簡單的樣例程序中,就是這個響應式遊樂場。下載這個starter project,而後編譯而且運行它去校驗你已經正確安裝了全部的事情。
ReactivePlayground is a very simple app that presents a sign-in screen to the user. Supply the correct credentials, which are, somewhat imaginatively, user for the username, and password for the password, and you’ll be greeted by a picture of a lovely little kitten.
ReactivePlayground 是一個很是簡單的app表明了一個sign-in屏幕給用戶。提供正確的憑證,有點想象力,用戶名是user, 密碼是 password, 而後你將會看到一隻可愛的小貓的歡迎頁。
Awww! How cute!
喔!多麼可愛!
Right now it’s a good point to spend a little time looking through the code of this starter project. It is quite simple, so it shouldn’t take long.
如今是時候花費一些時間去看一下這個項目的代碼。它很是簡單,因此這不用花很長時間。
Open RWViewController.m and take a look around. How quickly can you identify the condition that results in the enabling of the Sign In button? What are the rules for showing / hiding the
signInFailure
label? In this relatively simple example, it might take only a minute or two to answer these questions. For a more complex example, you should be able to see how this same type of analysis might take quite a bit longer.
打開RWViewController.m大概看一下。 您能多快識別出產生啓用Sign In按鈕的條件? 顯示/隱藏signInFailure標籤的規則是什麼? 在這個相對簡單的例子中,回答這些問題可能只須要一兩分鐘。 對於更復雜的示例,您應該可以看到相同類型的分析可能須要更長的時間。
With the use of ReactiveCocoa, the underlying intent of the application will become a lot clearer. It’s time to get started!
隨着ReactiveCocoa的使用,這個程序的潛在乎圖會更加清楚。讓咱們開始吧!
The easiest way to add the ReactiveCocoa framework to your project is via CocoaPods. If you’ve never used CocoaPods before it might make sense to follow the Introduction To CocoaPods tutorial on this site, or at the very least run through the initial steps of that tutorial so you can install the prerequisites.
最容易的增長ReactiveCocoa框架到你的項目中的方法是經過CocoaPods。若是你沒有使用過它,那麼遵循着Introduction To CocoaPods的指導是有意義的,或者至少運行一下那個指導的初始步驟,因此你就能夠安裝依賴了。
Note: If for some reason you don’t want to use CocoaPods you can still use ReactiveCocoa, just follow the Importing ReactiveCocoa steps in the documentation on GitHub.
注意:若是由於什麼緣由,你不想使用CocoaPods, 你仍然可使用ReactiveCocoa, 只要跟着在Github上的Importing ReactiveCocoa的文檔的步驟。
If you still have the ReactivePlayground project open in Xcode, then close it now. CocoaPods will create an Xcode workspace, which you’ll want to use instead of the original project file.
若是你的ReactivePlayground項目是打開的,那麼如今關上它。CocoaPods將會建立一個Xcode工做空間,你可使用那個來替換本來的項目文件。
Open Terminal. Navigate to the folder where your project is located and type the following:
打開終端。切換到項目目錄,輸入如下命令:
touch Podfile open -e Podfile
This creates an empty file called Podfile and opens it with TextEdit. Copy and paste the following lines into the TextEdit window:
這將建立一個空的名字是Podfile的文件,用文本編輯器打開它。複製而且貼入下面的代碼:
platform :ios, '7.0' pod 'ReactiveCocoa', '2.1.8'
This sets the platform to iOS, the minimum SDK version to 7.0, and adds the ReactiveCocoa framework as a dependency.
這個設置了iOS的平臺,最低SDK版本是7.0,而且增長了ReactiveCocoa框架做爲依賴。
Once you’ve saved this file, go back to the Terminal window and issue the following command:
一旦你保存了這個文件,回到終端,輸入下面的命令:
pod install
You should see an output similar to the following:
你將會看到相似如下的輸出:
Analyzing dependencies Downloading dependencies Installing ReactiveCocoa (2.1.8) Generating Pods project Integrating client project [!] From now on use `RWReactivePlayground.xcworkspace`.
This indicates that the ReactiveCocoa framework has been downloaded, and CocoaPods has created an Xcode workspace to integrate the framework into your existing application.
這意味着ReactiveCocoa框架已經被下載,而且CocoaPods建立了一個Xcode工做空間,集成了框架到你的程序中。
Open up the newly generated workspace, RWReactivePlayground.xcworkspace, and look at the structure CocoaPods created inside the Project Navigator:
打開新生成的工做空間,RWReactivePlayground.xcworkspace,看一下 CocoaPods 在項目瀏覽器中建立的結構:
You should see that CocoaPods created a new workspace and added the original project, RWReactivePlayground, together with a Pods project that includes ReactiveCocoa. CocoaPods really does make managing dependencies a breeze!
你應該看到 CocoaPods 建立了一個新的工做空間,而且增長了原始的項目,RWReactivePlayground,和一個包含着ReactiveCocoa的 Pods 項目在一塊兒。CocoaPods 確實使管理依賴變得垂手可得。
You’ll notice this project’s name is
ReactivePlayground
, so that must mean it’s time to play …
你將會注意到這個項目的名字是 ReactivePlayground, 因此是時候來玩了 ...
As mentioned in the introduction, ReactiveCocoa provides a standard interface for handling the disparate stream of events that occur within your application. In ReactiveCocoa terminology these are called signals, and are represented by the
RACSignal
class.
就像介紹裏面提到,ReactiveCocoa提供了一個標準的界面,來處理不一樣的產生自你的應用中的事件流。在ReactiveCocoa的術語中,它們被稱爲signal(信號,後文都稱爲signal),被RACSignal類所表明。
Open the initial view controller for this app, RWViewController.m, and import the ReactiveCocoa header by adding the following to the top of the file:
打開這個應用的初始的view controller,RWViewController.m, 引入ReactiveCocoa頭文件。
#import <ReactiveCocoa/ReactiveCocoa.h>
You aren’t going to replace any of the existing code just yet, for now you’re just going to play around a bit. Add the following code to the end of the
viewDidLoad
method:
你還不能替代現有的任何代碼,你能夠先運行一下。在viewDidLoad方法
底部加入下面的代碼:
[self.usernameTextField.rac_textSignal subscribeNext:^(id x) { NSLog(@"%@", x); }];
Build and run the application and type some text into the username text field. Keep an eye on the console and look for an output similar to the following:
編譯而且運行這個程序,在username文本域輸入一些字符。留心控制檯,會看到相似下面的輸出:
2013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i 2013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is 2013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is 2013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t 2013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th 2013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi 2013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is this 2013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is this 2013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is this m 2013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is this ma 2013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is this mag 2013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is this magi 2013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is this magic 2013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is this magic?
You can see that each time you change the text within the text field, the code within the block executes. No target-action, no delegates — just signals and blocks. That’s pretty exciting!
你能夠看到每一次你在這個文本域修改了文本,這個 block 中的代碼會運行。沒有target-action, 沒有delegates ----只有signal和 block。是否是很使人興奮!
ReactiveCocoa signals (represented by
RACSignal
) send a stream of events to their subscribers. There are three types of events to know: next, error and completed. A signal may send any number of next events before it terminates after an error, or it completes. In this part of the tutorial you’ll focus on the next event. Be sure to read part two when it’s available to learn about error and completed events.
ReactiveCocoa signals(被 RACSignal 所表明
)發送了一個事件流給它們的訂閱者。有三種類型的時間須要瞭解:next,error 和 completed。一個signal在它在一個error或者completed結束以前能夠發送任意數量的next事件。在這部分的指導中,你將會聚焦於next事件。當有條件去學習error和completed事件時,要保證去閱讀part two。
RACSignal
has a number of methods you can use to subscribe to these different event types. Each method takes one or more blocks, with the logic in your block executing when an event occurs. In this case, you can see that thesubscribeNext:
method was used to supply a block that executes on each next event.
RACSignal有不少方法,你能夠用來去訂閱這些不一樣的事件類型。每個方法接收一個或多個block,當事件發生時,block中的邏輯會被執行。在這種狀況下,您能夠看到subscribeNext:方法用於提供在每一個next事件上執行的塊。
The ReactiveCocoa framework uses categories to add signals to many of the standard UIKit controls so you can add subscriptions to their events, which is where the
rac_textSignal
property on the text field came from.
ReactiveCocoa框架使用了categories去給不少標準的 UIKit 組件增長signals,這樣你能夠給它們增長訂閱,這就是這個文本域的rac_textSignal域的來源。
But enough with the theory, it’s time to start making ReactiveCocoa do some work for you!
可是理論足夠(這裏爲何要用「可是」),是時候開始讓ReactiveCocoa爲你作一些工做了!
ReactiveCocoa has a large range of operators you can use to manipulate streams of events. For example, assume you’re only interested in a username if it’s more than three characters long. You can achieve this by using the
filter
operator. Update the code you added previously inviewDidLoad
to the following:
ReactiveCocoa有不少的操做符,你能夠用來操縱事件流。舉例來講,假設你只對3個字符以上的username感興趣。你能夠用filter 操做符來獲得這個。更新以前你加入到viewDidLoad的代碼:
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
If you build and run, then type some text into the text field, you should find that it only starts logging when the text field length is greater than three characters:
若是您構建並運行,而後在文本字段中鍵入一些文本,您會發現它只在文本字段長度大於三個字符時纔開始記錄:
2013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t 2013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th 2013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi 2013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is this 2013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is this 2013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is this m 2013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is this ma 2013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is this mag 2013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is this magi 2013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is this magic 2013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is this magic?
What you’ve created here is a very simple pipeline. It is the very essence of Reactive Programming, where you express your application’s functionality in terms of data flows.
你在這裏建立的是一個很是簡單的管道。 這是Reactive Programming的本質,您能夠根據數據流表達應用程序的功能。
It can help to picture these flows graphically:
用圖片來表示這個流是頗有幫助的:
In the above diagram you can see that the
rac_textSignal
is the initial source of events. The data flows through afilter
that only allows events to pass if they contain a string with a length that is greater than three. The final step in the pipeline issubscribeNext:
where your block logs the event value.
在上面這個圖中,你能夠看到rac_textSignal是事件的初始來源。數據流經過一個filter,僅僅容許包含字符串長度大於三的事件經過。管道的最後一步是
subscribeNext:
,那裏你的 block 輸出了事件的值。
At this point it’s worth noting that the output of the
filter
operation is also anRACSignal
. You could arrange the code as follows to show the discrete pipeline steps:
在這點上,值得注意的是,filter的輸出也是一個RACSignal。你也能夠像下面這樣組織代碼,來顯示離散的管道步驟:
RACSignal *usernameSourceSignal = self.usernameTextField.rac_textSignal; RACSignal *filteredUsername = [usernameSourceSignal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }]; [filteredUsername subscribeNext:^(id x) { NSLog(@"%@", x); }];
Because each operation on an
RACSignal
also returns anRACSignal
it’s termed a fluent interface. This feature allows you to construct pipelines without the need to reference each step using a local variable.
由於在一個RACSignal的每個操做都返回一個RACSignal,因此它被稱爲
fluent interface。這個特徵容許你去構建管道,而不須要使用一個本地變量去指向管道的每一步。
Note: ReactiveCocoa makes heavy use of blocks. If you’re new to blocks, you might want to read Apple’s Blocks Programming Topics. And if, like me, you’re familiar with blocks, but find the syntax a little confusing and hard to remember, you might find the amusingly titled f*****gblocksyntax.com quite useful! (We censored the word to protect the innocent, but the link is fully functional.)
注意:ReactiveCocoa大量地使用了 blocks。若是你對block不瞭解,你應該去閱讀蘋果的 Blocks Programming Topics。或者,像我同樣,你對blocks比較熟悉,可是發現這個語法有一些困擾,或者難於記憶,你能夠去看看 f*****gblocksyntax.com,會頗有幫助。(這個網址帶着一些髒字,爲了照顧讀者,因此這裏隱去了幾個字母,可是連接是徹底有效的。)
If you updated your code to split it into the various
RACSignal
components, now is the time to revert it back to the fluent syntax:
若是你更新你的代碼,把它分解成幾個RACSignal組件,如今是時候把它轉換成流利的語法:
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) { NSString *text = value; // implicit cast return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
The implicit cast from
id
toNSString
, at the indicated location in the code above, is less than elegant. Fortunately, since the value passed to this block is always going to be anNSString
, you can change the parameter type itself. Update your code as follows:
這個從id到 NSString的隱含轉換,在上面代碼的帶有註釋的哪一行,是不優雅的。幸運的是,由於傳遞給這個block的值永遠應該是一個NSString,因此你能夠修改入參類型,就像下面這樣:
[[self.usernameTextField.rac_textSignal filter:^BOOL(NSString *text) { return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
Build and run to confirm this works just as it did previously.
構建而且運行去確認它工做起來和以前是同樣的。
So far this tutorial has described the different event types, but hasn’t detailed the structure of these events. What’s interesting is that an event can contain absolutely anything!
到這裏,這份指導已經描述了不一樣的事件類型,可是尚未揭示這些事件的結構細節。有趣的是,一個事件能夠包含任何東西!
As an illustration of this point, you’re going to add another operation to the pipeline. Update the code you added to
viewDidLoad
as follows:
做爲對這點的一個闡述,你能夠給管道增長另一個操做。更新你以前增長的代碼:
[[[self.usernameTextField.rac_textSignal map:^id(NSString *text) { return @(text.length); }] filter:^BOOL(NSNumber *length) { return [length integerValue] > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
If you build and run you’ll find the app now logs the length of the text instead of the contents:
若是你構建而且運行它,你回發現如今程序打印的是文本的長度,而再也不是它的內容了:
2013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4 2013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5 2013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6 2013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7 2013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8 2013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9 2013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10 2013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11 2013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12
The newly added map operation transforms the event data using the supplied block. For each next event it receives, it runs the given block and emits the return value as a next event. In the code above, the map takes the
NSString
input and takes its length, which results in anNSNumber
being returned.
新增長的map操做使用提供的block轉換了事件的數據。對於每個它接收到的next事件,它運行給定的block,而且把返回值做爲 next事件發射出去。在上面的代碼中,map接收了NSString輸入,而且獲取了它的長度,用一個NSNumber返回出去。
For a stunning graphic depiction of how this works, take a look at this image:
有關其工做原理的精美圖像描述,請查看此圖像:
As you can see, all of the steps that follow the
map
operation now receiveNSNumber
instances. You can use themap
operation to transform the received data into anything you like, as long as it’s an object.
就像你能夠看到的,全部跟着map的步驟如今都接收到一個NSNumber實例
。你可使用map操做來轉換接收到的數據成你想要的任何東西,只要它是一個對象。
Note: In the above example the
text.length
property returns anNSUInteger
, which is a primitive type. In order to use it as the contents of an event, it must be boxed. Fortunately the Objective-C literal syntax provides and option to do this in a rather concise manner –@(text.length)
.
注意:在上面的例子中,這個text.length返回一個NSUInteger, 這是一個原始類型。爲了在一個事件的內容中使用它,它必須被包裹。幸運的是,
Objective-C literal syntax提供了一個很是簡潔的方式去處理這個 -- @(text.length)。(這裏好像是寫錯了「
provides and option」
彷佛不合語法)
That’s enough playing! It’s time to update the ReactivePlayground app to use the concepts you’ve learned so far. You may remove all of the code you’ve added since you started this tutorial.
這就足夠了! 是時候更新ReactivePlayground應用程序以使用您迄今爲止學到的概念。 您能夠刪除自本教程開始以來添加的全部代碼。
The first thing you need to do is create a couple of signals that indicate whether the username and password text fields are valid. Add the following to the end of
viewDidLoad
in RWViewController.m:
你須要作的第一件事情是建立一對信號,用來指示username和password 兩個文本域是否有效。在RWViewController.m 的 viewDidLoad
底部增長下面的代碼:
RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignal map:^id(NSString *text) { return @([self isValidUsername:text]); }]; RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal map:^id(NSString *text) { return @([self isValidPassword:text]); }];
As you can see, the above code applies a
map
transform to therac_textSignal
from each text field. The output is a boolean value boxed as aNSNumber
.
就像你看到的,上面代碼使用了一個 map 去給每個文本域轉換 rac_textSignal。輸出是被一個NSNumber包裹的布爾值。
The next step is to transform these signals so that they provide a nice background color to the text fields. Basically, you subscribe to this signal and use the result to update the text field background color. One viable option is as follows:
下一步是去轉換這些信號,使得它們能夠給這些文本域提供背景色。基本上,您訂閱此信號並使用結果更新文本字段背景顏色。 一個可行的選擇以下:
[[validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }] subscribeNext:^(UIColor *color) { self.passwordTextField.backgroundColor = color; }]; (Please don’t add this code, there’s a much more elegant solution coming!)
Conceptually you’re assigning the output of this signal to the
backgroundColor
property of the text field. However, the code above is a poor expression of this; it’s all backwards!
從概念上講,您將此信號的輸出分配給文本字段的background的Color屬性。 可是,上面的代碼對此表達的很差; 一切都倒退了!(不是很好的響應式的表達方式)
Fortunately, ReactiveCocoa has a macro that allows you to express this with grace and elegance. Add the following code directly beneath the two signals you added to
viewDidLoad
:
幸運的是,ReactiveCocoa有一個宏容許你用一種優雅的方式來表達。在你加入viewDidLoad的兩個信號下面直接加入下面的代碼:
RAC(self.passwordTextField, backgroundColor) = [validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }]; RAC(self.usernameTextField, backgroundColor) = [validUsernameSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }];
The
RAC
macro allows you to assign the output of a signal to the property of an object. It takes two arguments, the first is the object that contains the property to set and the second is the property name. Each time the signal emits a next event, the value that passes is assigned to the given property.
這個RAC宏容許你分配一個signal的輸出給一個對象的屬性。它使用了兩個參數,第一個是那個對象,包含着將要設置的屬性,第二個是屬性名。每一次signal發射一個next事件,被傳遞的值就被分配給給定的屬性。
This is a very elegant solution, don’t you think?
這是一種很是優雅的解決方案,你不以爲嗎?
One last thing before you build and run. Locate the
updateUIState
method and remove the first two lines:
你構建和運行前,最後一件事情,找到updateUIState方法,移除前面兩行:
self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor]; self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
That will clean up the non-reactive code.
這將清理非響應式的代碼。
Build and run the application. You should find that the text fields look highlighted when invalid, and clear when valid.
構建而且運行程序。你能夠發現文本域當無效時會變得高亮,有效時高亮會消失。
Visuals are nice, so here is a way to visualize the current logic. Here you can see two simple pipelines that take the text signals, map them to validity-indicating booleans, and then follow with a second mapping to a
UIColor
which is the part that binds to the background color of the text field.
可視化是很好的,因此這裏有一個辦法是把這個邏輯可視化。(用圖表的方式易於理解。)這裏你能夠看到兩個簡單的管道,接收文本signal,把他們map成指示有效的布爾型,而後再map成一個 UIColor,和文本域的
background的color屬性綁定起來。
Are you wondering why you created separate
validPasswordSignal
andvalidUsernameSignal
signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!
你是否會有疑問,爲何要建立validPasswordSignal和validUsernameSignal兩個信號,而不是爲建立一個流暢的管道?請有點耐心,這種處理背後的方法立刻就會變得清楚(就會出現了)。
In the current app, the Sign In button only works when both the username and password text fields have valid input. It’s time to do this reactive-style!
在當前的程序中,這個Sign In按鈕只在username和password兩個文本域都有有效的輸入時工做。是時候去把這個作成響應式了。
The current code already has signals that emit a boolean value to indicate if the username and password fields are valid;
validUsernameSignal
andvalidPasswordSignal
. Your task is to combine these two signals to determine when it is okay to enable the button.
當前的代碼已經有了signal發射了一個布爾值去指示着username和password域是否有效:validUsernameSignal和validPasswordSignal。你的任務是合併這兩個signal去決定何時可讓這個按鈕開始工做。
At the end of
viewDidLoad
add the following:
在viewDidLoad底部增長:
RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) { return @([usernameValid boolValue] && [passwordValid boolValue]); }];
The above code uses the
combineLatest:reduce:
method to combine the latest values emitted byvalidUsernameSignal
andvalidPasswordSignal
into a shiny new signal. Each time either of the two source signals emits a new value, the reduce block executes, and the value it returns is sent as the next value of the combined signal.
上面的代碼使用了combineLatest:reduce:
方法去合併由validUsernameSignal和validPasswordSignal發射的最後的值到一個閃亮的新的signal。每一次這兩個源signal中的一個發射出一個新值,這個reduce block就執行,而且它返回的值被做爲
combined signal的next的值。
Note: The
RACSignal
combine methods can combine any number of signals, and the arguments of the reduce block correspond to each of the source signals. ReactiveCocoa has a cunning little utility class,RACBlockTrampoline
that handles the reduce block’s variable argument list internally. In fact, there are a lot of cunning tricks hidden within the ReactiveCocoa implementation, so it’s well worth pulling back the covers!
注意:RACSignal方法能夠合併任意數量的信號,reduce block的參數對應於每一個源signal。 ReactiveCocoa有一個狡猾的小實用程序類RACBlockTrampoline,它在內部處理reduce block的變量參數列表。 事實上,在ReactiveCocoa實現中隱藏了許多小技巧,(最後一句沒搞明白)!
Now that you have a suitable signal, add the following to the end of
viewDidLoad
. This will wire it up to the enabled property on the button:
如今你有了一個合適的signal, 在viewDidLoad底部增長底下的代碼。這會將其鏈接到按鈕上的enabled屬性:
[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) { self.signInButton.enabled = [signupActive boolValue]; }];
Before running this code, it’s time to rip out the old implementation. Remove these two properties from the top of the file:
在運行這個代碼前,是時候移除舊的實現了。在文件頂部移除這兩個屬性:
@property (nonatomic) BOOL passwordIsValid; @property (nonatomic) BOOL usernameIsValid;
From near the top of
viewDidLoad
, remove the following:
在viewDidLoad頂部,移除底下的代碼:
// handle text changes for both text fields [self.usernameTextField addTarget:self action:@selector(usernameTextFieldChanged) forControlEvents:UIControlEventEditingChanged]; [self.passwordTextField addTarget:self action:@selector(passwordTextFieldChanged) forControlEvents:UIControlEventEditingChanged];
Also remove the
updateUIState
,usernameTextFieldChanged
andpasswordTextFieldChanged
methods. Whew! That’s a lot of non-reactive code you just disposed of! You’ll be thankful you did.
而且移除updateUIState
, usernameTextFieldChanged
和passwordTextFieldChanged
方法。喔!你剛剛移除了好多非響應式的代碼!你未來會感謝你所作的。
Finally, make sure to remove the call to
updateUIState
fromviewDidLoad
as well.
最後,確認從viewDidLoad移除掉對updateUIState的調用。
If you build and run, check the Sign In button. It should be enabled because the username and password text fields are valid, as they were before.
若是你構建而且運行了,檢查Sign In按鈕。若是username和password文本域有效,它將可使用,就像前面同樣。
An update to the application logic diagram gives the following:
更新應用的邏輯圖:
The above illustrates a couple of important concepts that allow you to perform some pretty powerful tasks with ReactiveCocoa;
上面描繪了兩個重要的概念,容許你用ReactiveCocoa去完成一些強大的任務;
- Splitting – signals can have multiple subscribers and serve as the source for more multiple subsequent pipeline steps. In the above diagram, note that the boolean signals that indicate password and username validity are split and used for a couple of different purposes.
- Combining – multiple signals may be combined to create new signals. In this case, two boolean signals were combined. However, you can combine signals that emit any value type.
The result of these changes is the application no longer has private properties that indicate the current valid state of the two text fields. This is one of the key differences you’ll find when you adopt a reactive style — you don’t need to use instance variables to track transient state.
這些改變的結果是應用程序再也不擁有私有屬性來指示這兩個文本域當前的這有效性。這是一個關鍵的不一樣,你將會發現當你採納響應式方式,你再也不須要實例變量去跟蹤瞬時狀態。
The application currently uses the reactive pipelines illustrated above to manage the state of the text fields and button. However, the button press handling still uses actions, so the next step is to replace the remaining application logic in order to make it all reactive!
當前的應用程序使用上面所描述的響應式的管道來管理文本域和按鈕的狀態。而後,按鈕的按下處理仍舊使用了actions, 因此下一步是取代這些剩餘的邏輯,來讓整個程序都變成響應式。
The Touch Up Inside event on the Sign In button is wired up to the
signInButtonTouched
method inRWViewController.m
via a storyboard action. You’re going to replace this with the reactive equivalent, so you first need to disconnect the current storyboard action.
這個在Sign In按鈕上的Touch Up Inside事件被經過storyboard的action綁定在RWViewController.m
的signInButtonTouched方法上。你將使用響應式的等價物來要替代這些,因此你先要斷開當前的
storyboard的action。
Open up Main.storyboard, locate the Sign In button, ctrl-click to bring up the outlet / action connections and click the x to remove the connection. If you feel a little lost, the diagram below kindly shows where to find the delete button:
打開Main.storyboard, 找到Sign In按鈕,按下ctrl和左鍵來打開outlet / action鏈接,而且點擊那個x來移除這個鏈接。若是你沒跟上,看下圖,能夠找到那個delete按鈕。
You’ve already seen how the ReactiveCocoa framework adds properties and methods to the standard UIKit controls. So far you’ve used
rac_textSignal
, which emits events when the text changes. In order to handle events you need to use another of the methods that ReactiveCocoa adds to UIKit,rac_signalForControlEvents
.
你能夠看到了ReactiveCocoa框架如何給標準的UIKit控件增長屬性和方法了。到如今爲止,你已經使用了rac_textSignal, 它會在文本改變時發射事件。爲了處理這些事件,你須要使用
ReactiveCocoa增長給UIKit的另一個方法,rac_signalForControlEvents。
Returning to
RWViewController.m
, add the following to the end ofviewDidLoad
:
返回RWViewController.m, 在viewDidLoad的地步增長下面的代碼:
[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) { NSLog(@"button clicked"); }];
The above code creates a signal from the button’s
UIControlEventTouchUpInside
event and adds a subscription to make a log entry every time this event occurs.
上面的代碼從按鈕的UIControlEventTouchUpInside
建立了一個signal,而且增長了一個訂閱,在每一次事件發生時輸出一條日誌。
Build and run to confirm the message actually logs. Bear in mind that the button will enable only when the username and password are valid, so be sure to type some text into both fields before tapping the button!
構建而且運行代碼來確認消息實際上輸出了。請記住,只有當用戶名和密碼有效時纔會啓用該按鈕,所以請務必在點擊按鈕以前在兩個字段中鍵入一些文本!
You should see messages in the Xcode console similar to the following:
你能夠在Xcode的控制檯上看到相似下面的輸出。
2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked 2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked 2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked 2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked 2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked
Now that the button has a signal for the touch event, the next step is to wire this up with the sign-in process itself. This presents something of a problem — but that’s good, you don’t mind a problem, right? Open up
RWDummySignInService.h
and take a look at the interface:
注意這個按鈕有一個touch事件的signal, 下一步就是把它綁定到sign-in過程當中去。這提出了一個問題 - 但這很好,你不介意一個問題,對吧? 打開RWDummySignInService.h並查看界面:
typedef void (^RWSignInResponse)(BOOL); @interface RWDummySignInService : NSObject - (void)signInWithUsername:(NSString *)username password:(NSString *)password complete:(RWSignInResponse)completeBlock; @end
This service takes a username, a password and a completion block as parameters. The given block is run when the sign-in is successful or when it fails. You could use this interface directly within the
subscribeNext:
block that currently logs the button touch event, but why would you? This is the kind of asynchronous, event-based behavior that ReactiveCocoa eats for breakfast!
此服務將用戶名,密碼和完成塊做爲參數。 登陸成功或失敗時運行給定的塊。 您能夠直接在當前記錄按鈕觸摸事件的subscribeNext:塊中使用此接口,但爲何會這樣?
Note: A dummy service is being used in this tutorial for simplicity, so that you don’t have any dependencies on external APIs. However, you’ve now run up against a very real problem, how do you use APIs not expressed in terms of signals?
注意:爲簡單起見,本教程中使用了虛擬服務,所以您對外部API沒有任何依賴性。 可是,您如今遇到了一個很是現實的問題,您如何使用未表達信號的API?
Fortunately, it’s rather easy to adapt existing asynchronous APIs to be expressed as a signal. First, remove the current
signInButtonTouched:
method from the RWViewController.m. You don’t need this logic as it will be replaced with a reactive equivalent.
幸運的是,將已經存在的異步API調整表達爲一個signal是一件很容易的事情。首先,從RWViewController.m文件中移除當前的signInButtonTouched:方法。你再也不須要這個邏輯,由於它將被一個響應式的等價物所替代。
Stay in RWViewController.m and add the following method:
留在RWViewController.m這個文件而且增長下面的方法:
-(RACSignal *)signInSignal { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [self.signInService signInWithUsername:self.usernameTextField.text password:self.passwordTextField.text complete:^(BOOL success) { [subscriber sendNext:@(success)]; [subscriber sendCompleted]; }]; return nil; }]; }
The above method creates a signal that signs in with the current username and password. Now for a breakdown of its component parts.
上面的方法建立了一個signal,發射於當前的用戶名和密碼。如今咱們分解它的組成部分。
The above code uses the
createSignal:
method onRACSignal
for signal creation. The block that describes this signal is a single argument, and is passed to this method. When this signal has a subscriber, the code within this block executes.
上面的代碼使用了RACSignal的createSignal:,爲了來建立signal。描述了這個signal的block是一個參數,傳遞給這個方法。當這個signal有一個訂閱者,這個block中的代碼將會被執行。block這裏所指的是這一段:
^RACDisposable *(id<RACSubscriber> subscriber) { [self.signInService signInWithUsername:self.usernameTextField.text password:self.passwordTextField.text complete:^(BOOL success) { [subscriber sendNext:@(success)]; [subscriber sendCompleted]; }]; return nil; }
這是一個block,返回值是RACDisposable *,參數是id<RACSubscriber> subscriber,實現了RACSubscriber協議的subscriber對象。這個地方須要反覆理解block的語法和做用,不然很容易不理解。把block當作一個匿名函數來理解,這裏體現的就是函數式編程,傳遞進去的參數再也不是一個變量,而是一個函數,接受這個參數的方法,再也不是對這個變量進行賦值、傳遞,而是對這個函數進行調用。
The block is passed a single
subscriber
instance that adopts theRACSubscriber
protocol, which has methods you invoke in order to emit events; you may also send any number of next events, terminated with either an error or complete event. In this case, it sends a single next event to indicate whether the sign-in was a success, followed by a complete event.
這個block被傳遞了一個subscriber實例,實現了RACSubscriber協議,這個協議擁有一些方法,使得你能夠調用去發射event;你也能夠發射任意數量的
next事件,最後終止於一個error或者complete事件。在這個案例中,它發送了一個next事件,指明瞭sign-in是否成功,跟隨者一個complete事件。
The return type for this block is an
RACDisposable
object, and it allows you to perform any clean-up work that might be required when a subscription is cancelled or trashed. This signal does not have any clean-up requirements, hencenil
is returned.
這個block的返回類型是一個RACDisposable對象,它容許你去執行一些清理工做--當一個
subscription(訂閱)被取消或者被丟棄時須要進行的。這裏的這個signal沒有清理的需求,因此返回了一個nil。
As you can see, it’s surprisingly simple to wrap an asynchronous API in a signal!
就像你所看到的,在一個signal中包裹一個異步的API是使人驚訝的簡單容易。
Now to make use of this new signal. Update the code you added to the end of
viewDidLoad
in the previous section as follows:
如今來使用這個signal。更新你剛纔增長在viewDidLoad底部的代碼:
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id x) { return [self signInSignal]; }] subscribeNext:^(id x) { NSLog(@"Sign in result: %@", x); }];
The above code uses the
map
method used earlier to transform the button touch signal into the sign-in signal. The subscriber simply logs the result.
上面的代碼使用前面使用過的map方法來把按鈕接觸的signal轉換成一個sign-in的signal,subscriber簡單地打印出結果。
If you build and run, then tap the Sign In button, and take a look at the Xcode console, you’ll see the result of the above code …
若是你構造了而且運行,而後點擊Sign In按鈕,看一下Xcode的控制檯,你會看到上面代碼的結果...
… and the result isn’t quite what you might have expected!
... 並非你想要的!
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result: <RACDynamicSignal: 0xa068a00> name: +createSignal:
The
subscribeNext:
block has been passed a signal all right, but not the result of the sign-in signal!
這個subscribeNext:
block被傳遞了一個signal,但不是sign-in signal的結果!
Time to illustrate this pipeline so you can see what’s going on:
到了該說明這個管道的時候了,這樣你就能夠看到發生了什麼:
The
rac_signalForControlEvents
emits a next event (with the sourceUIButton
as its event data) when you tap the button. The map step creates and returns the sign-in signal, which means the following pipeline steps now receive aRACSignal
. That is what you’re observing at thesubscribeNext:
step.
當你點擊這個按鈕的時候,這個rac_signalForControlEvents發射了一個next事件(伴隨着源
UIButton做爲它的事件的數據
)。map這一步建立而且返回這個
sign-in signal,這意味着後面的管道這一步如今能夠接收一個RACSignal。這就是你在 subscribeNext: 這一步所觀察到的。
The situation above is sometimes called the signal of signals; in other words an outer signal that contains an inner signal. If you really wanted to, you could subscribe to the inner signal within the outer signal’s
subscribeNext:
block. However it would result in a nested mess! Fortunately, it’s a common problem, and ReactiveCocoa is ready for this scenario.
上面這種狀況有時候被稱爲 signal of signals (信號的信號);換句話說,一個外部的signal包含了一個內部的signal。若是你真的想要,你能夠訂閱在外部的signal的subscribeNext:
塊裏面的內部的signal。而後它會致使一個嵌套混亂!幸運的是,這是一個很普通的問題,ReactiveCocoa已經爲這種場景作好了準備。
The solution to this problem is straightforward, just change the
map
step to aflattenMap
step as shown below:
這個問題的解決方案是很直接的,僅僅修改map這一步爲flattenMap,就像下面:
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id(id x) { return [self signInSignal]; }] subscribeNext:^(id x) { NSLog(@"Sign in result: %@", x); }];
This maps the button touch event to a sign-in signal as before, but also flattens it by sending the events from the inner signal to the outer signal.
這把按鈕的點擊事件映射成一個sign-in信號,而且把它flattens, 經過發送事件從內部的signal 到外部的signal。(flatten是扁平化的意思,這裏直接翻譯成扁平化,不太好理解,因此仍是用原詞)
Build and run, and keep an eye on the console. It should now log whether the sign-in was successful or not:
編譯而且運行,看一下控制檯。如今它會輸出sign-in是否成功:
2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0 2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1
Exciting stuff!
使人興奮吧!
Now that the pipeline is doing what you want, the final step is to add the logic to the
subscribeNext
step to perform the required navigation upon successful sign-in. Replace the pipeline with the following:
如今,管道作了你想作的,最後一步是給subscribeNext這一步增長邏輯去執行在成功
sign-in後所須要的導航。替換管道以下:
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^id(id x) { return [self signInSignal]; }] subscribeNext:^(NSNumber *signedIn) { BOOL success = [signedIn boolValue]; self.signInFailureText.hidden = success; if (success) { [self performSegueWithIdentifier:@"signInSuccess" sender:self]; } }];
The
subscribeNext:
block takes the result from the sign-in signal, updates the visibility of thesignInFailureText
text field accordingly, and performs the navigation segue if required.
這個subscribeNext: 塊使用了
sign-in signal的結果,所以更新了signInFailureText文本域的可見性,而且執行了須要的導航。
Build and run to enjoy the kitten once more! Meow!
編譯而且運行,再一次去享受這個小貓程序吧!喵!
Did you notice there is one small user experience issue with the current application? When the sign-in service validates the supplied credentials, is should disable the Sign In button. This prevents the user from repeating the same sign-in. Furthermore, if a failed sign-in attempt occurred, the error message should be hidden when the user tries to sign-in once again.
你有沒有注意到,當前的程序有一個很小的用戶體驗問題?當sign-in service確認了所支持的憑證,是應該使Sign In按鈕無效。這防止了用戶重複登陸。更進一步,若是一個失敗的sign-in嘗試發生,錯誤信息應該在用戶嘗試再次登陸時隱藏。
But how should you add this logic to the current pipeline? Changing the button’s enabled state isn’t a transformation, filter or any of the other concepts you’ve encountered so far. Instead, it’s what is known as a side-effect; or logic you want to execute within a pipeline when a next event occurs, but it does not actually change the nature of the event itself.
可是你應該怎麼去給當前的管道增長這個邏輯呢?修改這個按鈕的enabled狀態並非一個轉換,過濾或者任何你到如今爲止接觸到的概念。取而代之的,它是一個被稱爲side-effect反作用,這裏沒有貶義的意思)的概念。
Replace the current pipeline with the following:
像下面這樣修改管道:
[[[[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]; } }];
You can see how the above adds a
doNext:
step to the pipeline immediately after button touch event creation. Notice that thedoNext:
block does not return a value, because it’s a side-effect; it leaves the event itself unchanged.
你能夠看到上面是如何在按鈕點擊事件建立以後添加一個doNext:步驟給管道的。注意這個doNext:塊不返回值,由於它是一個反作用;它留下了沒有修改的事件。
The
doNext:
block above sets the button enabled property toNO
, and hides the failure text. Whilst thesubscribeNext:
block re-enables the button, and either displays or hides the failure text based on the result of the sign-in.
這個doNext:塊設置了按鈕的enabled屬性爲NO,而且隱藏了失敗的文本。同時subscribeNext:從新使按鈕生效,而且根據
sign-in的結果或者顯示或者隱藏失敗的文本。
It’s time to update the pipeline diagram to include this side effect. Bask in all it’s glory:
是時候更新管道的圖來包含這個反作用了。(Bask in all it’s glory這個意思沒有翻譯,尚未徹底搞清楚)
Build and run the application to confirm the Sign In button enables and disables as expected.
編譯而且運行這個程序去確認這個Sign In button像所期待的那樣有效或者無效。
And with that, your work is done – the application is now fully reactive. Woot!
而且,伴隨着這個,你的工做也完成了-- 這個程序已是徹底的響應式了。
If you got lost along the way, you can download the final project (complete with dependencies), or you can obtain the code from GitHub, where there is a commit to match each build and run step in this tutorial.
若是你沒有跟上文章的思路,你能夠下載final project最後的完整的工程),或者你能夠從GitHub上去獲取代碼,裏面有一個提交是匹配這篇知道里面的每個編譯和運行步驟。
Note: Disabling buttons while some asynchronous activity is underway is a common problem, and once again ReactiveCocoa is all over this little snafu. The
RACCommand
encapsulates this concept, and has anenabled
signal that allows you to wire up the enabled property of a button to a signal. You might want to give the class a try.
注意:在進行某些異步活動時禁用按鈕是一個常見問題,and once again ReactiveCocoa is all over this little snafu(後一句暫不會翻譯)。這個RACCommand封裝了這個概念,有一個enabled signal容許你鏈接一個按鈕的enabled屬性到一個 signal。
You might want to give the class a try()。
Hopefully this tutorial has given you a good foundation that will help you when starting to use ReactiveCocoa in your own applications. It can take a bit of practice to get used to the concepts, but like any language or program, once you get the hang of it it’s really quite simple. At the very core of ReactiveCocoa are signals, which are nothing more than streams of events. What could be simpler than that?
但願這個指導能給你一個很好的基礎,幫助你開始在你的程序中使用ReactiveCocoa 。這須要你採用更多的練習去熟悉這些概念,就像任何語言或者程序同樣,一旦你掌握了,它實際上很簡單。ReactiveCocoa的核心是信號,它們只不過是事件流。 有什麼比這更簡單?
With ReactiveCocoa one of the interesting things I have found is there are numerous ways in which you can solve the same problem. You might want to experiment with this application, and adjust the signals and pipelines to change the way they split and combine.
採用ReactiveCocoa, 一個有意思的事情是,將會有不少種方法你來解決一個問題。你能夠在這個程序裏面去實驗它,調整信號和管道的拆分和合並的方式。
It’s worth considering that the main goal of ReactiveCocoa is to make your code cleaner and easier to understand. Personally I find it easier to understand what an application does if its logic is represented as clear pipelines, using the fluent syntax.
值得思考的是,ReactiveCocoa的主要目標是讓你的代碼乾淨和容易被理解。就我我的而言,若是使用流暢的語法將其邏輯表示爲清晰的管道,我會發現更容易理解應用程序的做用。
In the second part of this tutorial series you’ll learn about more advanced subjects such as error handing and how to manage code that executes on different threads. Until then, have fun experimenting!
在這篇指導系列的第二部分,你將會學到更多高級的課題,例如錯誤處理和如何去管理代碼去在不一樣的線程上執行。在那以前,開心地實驗吧!