ReactiveCocoa學習筆記(1)


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,我們需要拿到xmap上面說過了,是用來轉換對象類型的,也就是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 :]


博主原創,轉載請注明出處,不勝感激

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容

  • 一篇關于RectiveCocoa的總結文檔 百度搜索了一下RectiveCocoa,都是與MVVM關聯在一起。 1...
    毒某人閱讀 486評論 0 0
  • 一.編程思想 先簡單介紹下目前咱們已知的編程思想。1.面向過程:處理事情以過程為核心,一步一步的實現。C語言是面向...
    門前有棵葡萄樹閱讀 265評論 0 1
  • 作為一個iOS開發者,你寫的每一行代碼幾乎都是在相應某個事件,例如按鈕的點擊,收到網絡消息,屬性的變化(通過KVO...
    jiajia1118閱讀 813評論 0 2
  • RAC支持的UI控件 RACCommand RACCommand類用于表示事件的執行,一般來說是在UI上的某些動作...
    花前月下閱讀 2,762評論 0 5
  • 1.ReactiveCocoa常見操作方法介紹。 1.1 ReactiveCocoa操作須知 所有的信號(RACS...
    萌芽的冬天閱讀 1,034評論 0 5