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 :]
博主原创,转载请注明出处,不胜感激