title: ReactiveCocoa學習筆記(1)
date: 2016-11-16 18:06:05
categories:
- iOS_SHAKALAKA
tags: - ReactiveCocoa
- iOS
- FRP
- 函數響應式編程
寫在前面的話
說來慚愧,已經很長時間沒有寫新的文章了,原因有很多,換了新工作,然后正好趕上畢業,搬家,亂七八糟一堆事,嗯,各方面的原因......
。
。。
。。。
。。。。
。。。。。
。。。。。。
其實,以上都是屁話,就是因為懶。
還好英明神武如我,機智的意識到了這個問題,所以決定重拾自己的小博客,一如既往,分享自己在iOS開發道路上遇到的一些小故事。
關于ReactiveCocoa
按照套路,一般都會講述一段RAC(ReactiveCocoa簡稱)的前生今世,還有筆者與之或深或淺糾葛之類的,為了不落俗套,所以這一段跳過。如果有讀者想要了解,請出門右拐Google一發,或者官方git地址戳我。
往下看之前,本文假定讀者已經知道了RAC的基本常識,生成信號、訂閱信號、發送信號、冷熱信號的大概意思...... - 。-
由于筆者本人也是RAC初學者,所以撰文如果有什么錯誤之處(簡直廢話)請及時評論指教,吾將不勝感激。
正文
項目引入RAC
截止發文時間,RAC官方最新版本5.0.0-alpha.3,本次大版本迭代,有了很明顯的改動,將RAC拆分成四個模塊:
- ReactiveCocoa
- ReactiveSwift
- ReactiveObjc
- ReactiveObjCBridge
由于公司項目是純OC項目,暫時不討論純swift或者混編的情況 -。-
RAC目前支持純OC的最高版本是2.5,因此cocoapods導入命令如下:
pod 'ReactiveCocoa', '~> 2.5'
pch文件導入
#import <ReactiveCocoa/ReactiveCocoa.h>
很簡單,到這里我們的項目就成功接入了RAC(OC部分)。
項目使用RAC
如果你在看本文之前已經,百度或者google了不少關于RAC初級教程,你會漸漸發現他們有一個共同點,就是他們的demo里通常會是一個關于登錄頁面的例子,或者是關于UITextField的控件使用RAC的例子,你可能已經看吐了,(反正我是看吐了)。不過也不奇怪,UITextField確實是一個能比較直觀展現RAC強大功能的控件,有UI有交互,很清晰的展示邏輯。所以,下面本人總結了眾多前輩關于UITextField使用RAC的多種技巧方式,我將逐一介紹:
對了,悄悄分享一個關于Xcode打印的第三方小工具,真的很好用 :) LxDBAnything
/*
controller 里先定義兩個控件,賬號textField,密碼textField
*/
@property (nonatomic, strong) UITextField *accountTextField;
@property (nonatomic, strong) UITextField *passwordTextField;
then
//訂閱textField的text
[self.accountTextField.rac_textSignal subscribeNext:^(id x) {
LxDBAnyVar(x);
//自定義操作
}];
這段代碼,實際上就是監聽了textField的text改變回調。這里的rac_textSignal是RAC給我們提供的一個UITextField的category,目的就是方便開發者調用,值得一提的是,RAC給開發者提供了很多類似的category,我們在學習使用RAC的過程中就會漸漸熟悉。但是蘋果的SDK能玩出的花樣實在太多,RAC官方不可能什么都包涵,所以需要我們掌握一定的原理,必要的時候,可以自己創建滿足自身業務需求的信號。
好了,言歸正傳,我們注意到rac_textSignal,這是個什么東西?實際上,這就是RAC幫我們生成的一個關于UITextField的text改變的信號,這是一個RACSignal(RAC的核心內容,即信號),subscribleNext又是什么呢?這是一個訂閱方法,也就是說,我們現在已經生成了信號,需要有人去關注這個信號(即訂閱),否則這個信號就是一個冷信號,很容易想到,沒人關心這個信號它當然沒什么用了。調用完subscribleNext方法,注意到這是一個block回調方式,回調類型是id,值是value,針對本例的控件是一個UITextField,我們也可以這樣寫:
[self.accountTextField.rac_textSignal subscribeNext:^(NSString *text) {
LxDBAnyVar(text);
//自定義操作
}];
顯而易見,我們拿到了text值。ok,讓我們來運行一下demo,輸入幾個字符試試看,嗯......
有打印值!!!額,不過,為什么打印了兩遍??不妨試試下面這段代碼:
[[self.accountTextField.rac_textSignal distinctUntilChanged] subscribeNext:^(NSString *text) {
LxDBAnyVar(text);
//自定義操作
}];
distinctUntilChanged 這個方法可以使我們的信號,只有在發生改變的情況下(這一次和上次的值不同)才去發送信號。
條件再苛刻一點,我們希望輸入的字符串長度大于6個字符時才接受信號,處理邏輯,該怎么辦呢?
[[[self.accountTextField.rac_textSignal distinctUntilChanged] filter:^BOOL(NSString *text) {
return text.length > 6;
}] subscribeNext:^(NSString *text) {
LxDBAnyVar(text);
//自定義操作
}];
注意到我們這次使用了filter,這個方法返回一個BOOL類型值,只有值為YES的情況下,我們才去調用subscribeNext訂閱它。
對于UITextField這個控件,有經驗的iOS開發者都知道這個控件有一個潛在的小問題,選擇鍵盤推薦的漢字,將不能觸發UITextField和UITextView的代理方法,這是蘋果的bug!所以目前只能采用通知監聽的辦法!因此,我們需要處理中文鍵盤的預輸入狀態,以前的OC做法如下:
UITextField處理代碼如下
//添加通知
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(textFiledEditChanged:)
name:UITextFieldTextDidChangeNotification object:self.textField];
- (void)textFiledEditChanged:(NSNotification *)notification
{
NSString *toBeString = self.textField.text;
UITextInputMode *currentInputMode = self.textField.textInputMode;
NSString *lang = [currentInputMode primaryLanguage]; // 鍵盤輸入模式
if ([lang isEqualToString:@"zh-Hans"]) { // 簡體中文輸入,包括簡體拼音,健體五筆,簡體手寫
UITextRange *selectedRange = [textView markedTextRange];
//獲取高亮部分
UITextPosition *position = [textView positionFromPosition:selectedRange.start offset:0];
// 沒有高亮選擇的字,則對已輸入的文字進行字數統計和限制
if (!position) {
if (toBeString.length > 140) {
textField.text = [toBeString substringToIndex:140];
}
}
// 有高亮選擇的字符串,則暫不對文字進行統計和限制
else{
}
}
// 中文輸入法以外的直接對其統計限制即可,不考慮其他語種情況
else{
if (toBeString.length > 140) {
textField.text = [toBeString substringToIndex:140];
}
}
}
UITextView處理代碼如下
//添加通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self.textView];
- (void)textViewDidChangeNotification:(NSNotification *)notification
{
NSString *toBeString = self.textView.text;
UITextInputMode *currentInputMode = textView.textInputMode;
NSString *lang = [currentInputMode primaryLanguage]; // 鍵盤輸入模式
if ([lang isEqualToString:@"zh-Hans"]) { // 簡體中文輸入,包括簡體拼音,健體五筆,簡體手寫
UITextRange *selectedRange = [textView markedTextRange];
//獲取高亮部分
UITextPosition *position = [textView positionFromPosition:selectedRange.start offset:0];
// 沒有高亮選擇的字,則對已輸入的文字進行字數統計和限制
if (!position) {
if (toBeString.length > 140) {
textView.text = [toBeString substringToIndex:140];
}
}
// 有高亮選擇的字符串,則暫不對文字進行統計和限制
else{
}
}
// 中文輸入法以外的直接對其統計限制即可,不考慮其他語種情況
else{
if (toBeString.length > 140) {
textView.text = [toBeString substringToIndex:140];
}
}
}
很麻煩不是么?!RAC該怎么辦?仔細看一遍OC做法,我們可以發現,無非就是拿到textField狀態去做判斷,忽略掉中文預輸入狀態。因此,RAC情況下我們需要在text被改變的時候,不僅要拿到text的值,也要拿到textField自身,可是之前的rac_textSignal只能返回text啊,怎么辦?別急,我們來看一下rac_textSignal是怎么生成的。
剖析一下rac_textSignal源碼:
@weakify(self);
return [[[[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self];
}]
concat:[self rac_signalForControlEvents:UIControlEventAllEditingEvents]]
map:^(UITextField *x) {
return x.text;
}]
takeUntil:self.rac_willDeallocSignal]
setNameWithFormat:@"%@ -rac_textSignal", self.rac_description];
很幸運,只有短短10余行代碼,[self rac_signalForControlEvents:UIControlEventAllEditingEvents] 這句代碼的意思是,獲取UITextField處于編輯狀態下生成的信號,UIControlEventAllEditingEvents其實是一個位掩碼(bitmask),即用戶對UITextField編輯的所有狀態,map是一個轉換信號的函數(很重要,下面會單獨講解),takeUntil我猜是信號發送時機,字面看來,是信號被dealloc后就不發送了(好像是廢話-。-),setNameWithFormat讓我想到了以前想打印某個類需要重寫description方法。
觀察結果:map函數返回的是 return x.text,我們需要拿到x,map上面說過了,是用來轉換對象類型的,也就是UITextField x這個參數是轉換而來的,是誰轉換來的呢?把目光聚焦到這句代碼,[self rac_signalForControlEvents:UIControlEventAllEditingEvents]*,是的,就是這句代碼!
直接上代碼:
//account輸入框 (password輸入框邏輯相同)
//長度限制:不得少于6個字符,不得超過10個字符,
//背景色:少于6個字符,背景色為灰色,否則為黃色
[[[self.accountTextField rac_signalForControlEvents:UIControlEventAllEditingEvents]
filter:^BOOL(UITextField *accountTextField) {
//過濾掉中文預輸入狀態
if (accountTextField.markedTextRange == nil) {
return YES;
} else {
return NO;
}
}] subscribeNext:^(UITextField *accountTextField) {
@strongify(self);
//訂閱熱信號到這里,此時已經過濾掉中文預輸入狀態的情況了,此處需要過濾掉空格
NSString *account = [accountTextField.text stringByReplacingOccurrencesOfString:@" " withString:@""];
NSUInteger length = account.length;
if (length < 6) {
self.accountTextField.backgroundColor = [UIColor grayColor];
self.accountTextField.text = account;
} else if (length >= 6 && length <=10) {
self.accountTextField.backgroundColor = [UIColor yellowColor];
self.accountTextField.text = account;
} else {
self.accountTextField.backgroundColor = [UIColor yellowColor];
self.accountTextField.text = [account substringToIndex:10];
}
}];
運行這段代碼會發現,我們已經拿到了textFiedl自身,如何產生的?UIControlEventAllEditingEvents是UITextField被點擊或者編輯的狀態,一旦處于該狀態,就會觸發產生該信號,再使用filter過濾掉中文預輸入狀態,此時的信號就是我們需要處理的,這個時候調用subscribeNext去訂閱,直接在block回調里做我們的自定義操作即可!
關于UITextField的講解大概就是上面這些了,相信這些簡單小問題你早就看得不耐煩了,接下來,我們再來講一個有趣而且實用的案例。
我們實際項目開發里,通常賬號框和密碼框如果輸入了合法的字符串長度,我們希望登錄按鈕是可用的(即Normal狀態),否則是不可用的(即Disable狀態)。先大概說一下OC之前的做法,聲明兩個全局String屬性記錄account和password,在UITextField的delegate里分別實時修改,每次調用delegate結尾都需要走一個檢查方法,如果符合,修改按鈕狀態。
這樣做,一來是需要設置不同tag對應修改記錄NSString,二來是每次都去檢查,確實有點小麻煩,總感覺應該會有更好的解決方案。
比如RAC這樣:
/**
* 實現目的效果
* account 和 password 進行輸入合法性檢測,生成是否合法信號
* 如果兩個都合法,登錄按鈕置為可用狀態,否則不可用狀態
*/
//創建 account 信號 使用map轉換,輸出NSNumber類型對象
RACSignal *validAccountSignal = [[self.accountTextField rac_signalForControlEvents: UIControlEventAllEditingEvents] map:^id(UITextField *accountTextField) {
if (accountTextField.markedTextRange == nil) {
return @([self isValidAccount:accountTextField.text]);
} else {
return @(NO);
}
}];
//創建 password 信號
RACSignal *validPasswordSignal = [[self.passwordTextField rac_signalForControlEvents: UIControlEventAllEditingEvents] map:^id(UITextField *passwordTextField) {
if (passwordTextField.markedTextRange == nil) {
return @([self isValidPassword:passwordTextField.text]);
} else {
return @(NO);
}
}];
注意,這里我們使用了map方法,實際上我們希望轉換成BOOL類型,但是由于RAC的map轉換必須是OC對象,所以強行將UITextField信號參數轉換成NSNumber類型,這樣,我們已經產生了account是否合法信號和password是否合法信號,因為沒有訂閱,所以到這里還是冷信號。
繼續思考我們的問題,怎樣同時訂閱兩個信號并且兩個信號都合法我們修改按鈕狀態?然而對于登錄按鈕來說,它并不關心是幾個信號,它只關注
1、合法,Normal狀態
2、不合法,Disable狀態
別急,RAC給我們提供了combineLatest
/**
* 實現目的效果
* 如果 account 和 password 都合法,login 按鈕狀態置為可用,否則狀態置為不可用
*/
RACSignal *loginActiveSignal = [RACSignal combineLatest:@[validAccountSignal, validPasswordSignal]
reduce:^id (NSNumber *isValidaAccount,
NSNumber *isValidPassword){
return @(isValidaAccount.boolValue && isValidPassword.boolValue);
}];
這段代碼,將 account信號 和 password信號 聚合成一個新的信號,這兩個源信號任何一個發生改變,都會觸發聚合的新信號的 reduce block回調,在這個回調里, 我們只需要返回一個yes or no,供登錄按鈕訂閱該信號
//account信號 和 password信號 同時返回 1,則 login 按鈕狀態為yes
RAC(self.loginBtn, enabled) = [loginActiveSignal map:^id(NSNumber *isActiveLogin) {
return @(isActiveLogin.boolValue);
}];
咿?RAC()這個宏沒見過啊,其實,這是RAC給我們提供的一個簡單的綁定寫法,將訂閱到的熱信號直接和控件的屬性相互關聯起來,也就是信號的值直接且實時決定控件的某個屬性。
到了這里,我們很自然而然的想到點擊按鈕之后,我們需要發起一個登錄的網絡接口請求,記得以前做登錄的模塊,需要判斷的條件很多,網絡回調處理的邏輯也很麻煩,代碼寫的亂且零散,看的頭疼。那么,RAC這塊又是如何處理的呢?
[[[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
self.loginBtn.userInteractionEnabled = NO;
}] flattenMap:^RACStream *(id value) {
LxDBAnyVar(value);
return [self fetchLoginNetworkAPI];
}] subscribeNext:^(NSNumber *isLoginSucc) {
BOOL suceess = isLoginSucc.boolValue;
LxDBAnyVar(suceess ? @"login success!" : @"login failed!");
if (suceess) {
DemoVC_1 *vc = [DemoVC_1 new];
[self.navigationController pushViewController:vc animated:YES];
} else {
}
self.loginBtn.userInteractionEnabled = YES;
}];
哇!這次好像調用的方法有點多啊,別急,一個一個來。
rac_signalForControlEvents:UIControlEventTouchUpInside這句代碼相信就不用多解釋了,但凡點擊按鈕,就會觸發生成該信號。
doNext是干嘛的?這是為了防止用戶多次重復點擊登錄按鈕,想想你以前的代碼是如何處理的,再看看這里,是不是很簡單呢!
flattenMap是處理”信號中的信號“,哪來的這個怪東西?注意看,return [self fetchLoginNetworkAPI],這是我們寫的用戶登錄網絡請求方法(調API接口),它生成了一個關于調登錄接口的回調信號,此處作為參數,因此產生了信號中的信號,至于為什么使用flattenMap處理?我也不太懂,只知道這是正確做法(還需要多研究研究 -。-),subscribeNext很熟悉吧,訂閱它,并且在它的回調里做登錄成功或失敗的邏輯處理即可!
- (RACSignal *)fetchLoginNetworkAPI
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
/*
調用 login 網絡請求接口 ...
通常我們的網絡處理工具類都會有一個成功block和失敗block
*/
//成功回調如下
[subscriber sendNext:@(YES)];
[subscriber sendCompleted];
//失敗回調如下
// [subscriber sendNext:@(NO)];
// [subscriber sendCompleted];
return nil;
}];
}
結論:使用RAC處理登錄模塊的事件流和邏輯部分,大大方便了開發者,減少了一半的代碼量,且緊湊美觀,只需要關注業務邏輯部分,提高了開發效率。
總結
由于自己接觸RAC時間較短(兩天時間想想都佩服自己的勇氣居然敢寫),水平有限,且博客文章更多只是給自己當做學習筆記來用的,難免有錯誤理解以及不到之處,希望讀者朋友們多多指點,本文只是自己初步學習RAC的第一篇文章,希望下一篇質量能有較大提升。
See U Later :]
博主原創,轉載請注明出處,不勝感激