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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 一篇关于RectiveCocoa的总结文档 百度搜索了一下RectiveCocoa,都是与MVVM关联在一起。 1...
    毒某人阅读 477评论 0 0
  • 一.编程思想 先简单介绍下目前咱们已知的编程思想。1.面向过程:处理事情以过程为核心,一步一步的实现。C语言是面向...
    门前有棵葡萄树阅读 257评论 0 1
  • 作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO...
    jiajia1118阅读 796评论 0 2
  • RAC支持的UI控件 RACCommand RACCommand类用于表示事件的执行,一般来说是在UI上的某些动作...
    花前月下阅读 2,732评论 0 5
  • 1.ReactiveCocoa常见操作方法介绍。 1.1 ReactiveCocoa操作须知 所有的信号(RACS...
    萌芽的冬天阅读 1,013评论 0 5