分享

iOS开发

 jerry_tom123 2016-01-13

前言

本文是根据DeveloperLx在斗鱼直播ReactiveCocoa时整理的文章,同时加上本人的见解,若有错误希望指出。
感谢 DeveloperLx 为大家做出的贡献,虽然直播声音小了点,但满满的都是干货 ^v^

简介

神马是RAC?ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。借用RayWenderlich上面的话:

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.

翻译过来就是:

作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

RAC是由 Mattt Thompson 大神开发的,很多开发者对其的评价是开启一个新Objective-C纪元,可见对其评价有多高。
以下是RAC的Github主页:ReactiveCocoa
以及官方给出的用法链接

安装

ReactiveCocoa安装教程我就不说了,用pod安装即可。
本文的Demo可在文章最后下载,在阅读本文的时候,强烈推荐边看Demo边看博文。
项目中加入了ReactiveCocoa 和 DeveloperLx 大神的打印插件 LxDBAnything
同时加入了键盘相应的第三方IQKeyboardManager,然而没有怎么用到。
以及Masonry,使用方法可以看这篇文章,iOS - Masonry自动布局(Autolayout)

撸代码

第一部分 简单使用

文本框事件

原来我们在使用textFiled的时候我们需要写到

1
[textField addTarget:self action:@selector(textChanged:) forControlEvents:UIControlEventEditingChanged];

然后实现textChanged:方法,在RAC中,对于文本框的监听,是非常简单的一件事情,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UITextField * textField = ({
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];

textField;
});
[self.view addSubview:textField];

@weakify(self); // __weak __typeof__(self) self_weak_ = self;

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self); // __strong __typeof__(self) self = self_weak_;
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];

[[textField rac_signalForControlEvents:UIControlEventEditingChanged]
subscribeNext:^(id x) {

LxDBAnyVar(x);
}];
[textField.rac_textSignal subscribeNext:^(NSString *x) {

LxDBAnyVar(x);
}];

打印结果:

1
2
3
4
5
6
7
8
9
__31-[ViewController textFiledTest]_block_invoke_2 + 215 x = 12
__31-[ViewController textFiledTest]_block_invoke241 + 211 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
__31-[ViewController textFiledTest]_block_invoke_2 + 215 x = 123
__31-[ViewController textFiledTest]_block_invoke241 + 211 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '1231'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
__31-[ViewController textFiledTest]_block_invoke_2 + 215 x = 1231
__31-[ViewController textFiledTest]_block_invoke241 + 211 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '12312'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
__31-[ViewController textFiledTest]_block_invoke_2 + 215 x = 12312
__31-[ViewController textFiledTest]_block_invoke241 + 211 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
__31-[ViewController textFiledTest]_block_invoke_2 + 215 x = 123123

我们很容易的监听到textFiled中发生的变化,其中x的类型默认为id类型, 我们已知它的类型的时候我们可以将其改变,就像上面代码,将id改成了NSString类型。

手势

1
2
3
4
5
6
7
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]init];
[[tap rac_gestureSignal] subscribeNext:^(UITapGestureRecognizer * tap) {

LxDBAnyVar(tap);
}];
[self.view addGestureRecognizer:tap];

为了方便,我们直接添加到self.view上,点击屏幕,得到打印结果:

1
__29-[ViewController gestureTest]_block_invoke + 184 tap = <UITapGestureRecognizer: 0x7fa2e3e1f9f0; state = Ended; view = <UIView 0x7fa2e3e20b70>; target= <(action=sendNext:, target=<RACPassthroughSubscriber 0x7fa2e3c064f0>)>>

通知

1
2
3
4
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * notification) {

LxDBAnyVar(notification);
}]
;

我们建立了一个通知,叫做进入后台, 当程序进入后台的时候通知相应,当我们用RAC写通知的时候,我们有一个好处,就是不用removeObserver通知,因为RAC通知的监听者师RAC自己,它会帮你管理释放方法。可以看方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object {
@unsafeify(object);
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
@strongify(object);
id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) {
[subscriber sendNext:note];
}];

return [RACDisposable disposableWithBlock:^{
[self removeObserver:observer];
}];
}] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>", notificationName, [object class], object];
}

定时器

1
2
3
4
5
6
7
8
9
10
11
//1. 延迟某个时间后再做某件事
[[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{

LxPrintAnything(rac);

}];

//2. 每间隔多长时间做一件事
[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]subscribeNext:^(NSDate * date) {

LxDBAnyVar(date);

}];

这是定时器最常用的两种写法,第一种方法,延迟时间去做某件事,更改afterDelay的属性。
第二种方法,每间隔多长时间做一件事,更改interval属性。

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"RAC" message:@"ReactiveCocoa" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Ensure", nil];

[[self rac_signalForSelector:@selector(alertView:clickedButtonAtIndex:) fromProtocol:@protocol(UIAlertViewDelegate)] subscribeNext:^(RACTuple * tuple) {

LxDBAnyVar(tuple);

LxDBAnyVar(tuple.first);
LxDBAnyVar(tuple.second);
LxDBAnyVar(tuple.third);
}];
[alertView show];


// 更简单的方式:
[[alertView rac_buttonClickedSignal]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

用RAC去写代理的时候,会有局限,只能取代没有返回值的代理方法,什么是没有返回值的代理呢?比如说tableView的代理方法:

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

这两个方法一个返回的是CGFloat,一个是void,RAC只能取代void的代理。

KVO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UIScrollView * scrollView = [[UIScrollView alloc]init];
scrollView.delegate = (id<UIScrollViewDelegate>)self;
[self.view addSubview:scrollView];

UIView * scrollViewContentView = [[UIView alloc]init];
scrollViewContentView.backgroundColor = [UIColor yellowColor];
[scrollView addSubview:scrollViewContentView];

@weakify(self);

[scrollView mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(80, 80, 80, 80));
}];

[scrollViewContentView mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.edges.equalTo(scrollView);
make.size.mas_equalTo(CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)));
}];

[RACObserve(scrollView, contentOffset) subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

用RAC写KVO的好处就是方法简单,keypath有代码提示。

第二部分 进阶

信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (RACSignal *)loginSignal
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

RACDisposable * schedulerDisposable = [[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{

if (arc4random()%10 > 1) {

[subscriber sendNext:@"Login response"];
[subscriber sendCompleted];
}
else {

[subscriber sendError:[NSError errorWithDomain:@"LOGIN_ERROR_DOMAIN" code:444 userInfo:@{}]];
}
}];

return [RACDisposable disposableWithBlock:^{

[schedulerDisposable dispose];
}];
}];
}

RAC的核心就是RACSignal,也就是信号,我们可以直接创建信号createSignal,并发送它sendNext,当信号完成后我们同时用dispose方法销毁它。发送信号,我们同时也要订阅信号,订阅信号代码如下:

1
2
3
4
5
6
7
8
9
10
[signal subscribeNext:^(id x) {

LxDBAnyVar(x);
} error:^(NSError *error) {

LxDBAnyVar(error);
} completed:^{

LxPrintAnything(completed);
}]
;

在信号发送的时候, 错误的时候,以及完成的时候,我们都可以得到相应。

信号的处理

map (映射)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UITextField * textField = ({
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];

textField;
});
[self.view addSubview:textField];

@weakify(self); // __weak __typeof__(self) self_weak_ = self;

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self); // __strong __typeof__(self) self = self_weak_;
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];

[[textField.rac_textSignal map:^id(NSString *text) {

LxDBAnyVar(text);

return @(text.length);

}] subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

map这个函数,在这里不是地图的意思,代表映射。map能做的事情就是把监听的rac_textSignal所返回的值,替换成别的就像上面代码中的text的长度。

filter

为了方便演示,我就不再赋值创建textField的代码了,请到Demo中查看

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[textField.rac_textSignal map:^id(NSString *text) {

LxDBAnyVar(text);

return @(text.length);

}]
filter:^BOOL(NSNumber *value) {

return value.integerValue > 3;

}]
subscribeNext:^(id x) {
LxDBAnyVar(x);
}]
;

filter是个BOOL值,它代表的是一个条件,当这个条件发生的时候才会作出相应,比如上面代码中,当长度大于3的时候,才会打印x的值。

delay
1
2
3
4
5
6
7
8
9
10
11
//创建信号
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
return nil;
}]delay:2];
LxPrintAnything(start);
//创建订阅者
[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

delay的作用就是延迟,或者说等待,如上,等待2秒之后打印了x。

startWith
1
2
3
4
5
6
7
8
9
10
11
12
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// [subscriber sendNext:@"123"];//startWith:@"123"等同于这句话 也就是第一个发送,主要是位置
[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
return nil;
}]startWith:@"123"];
LxPrintAnything(start);
//创建订阅者
[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

startWith也就是最开始的意思,看以上代码 startWith:@"123"等同于[subscriber sendNext:@"123"] 也就是第一个发送,主要是位置.

timeOut
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

[[RACScheduler mainThreadScheduler]afterDelay:3 schedule:^{

[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
}]
;


return nil;
}]
timeout:2 onScheduler:[RACScheduler mainThreadScheduler]]

subscribeNext:^(id x) {

LxDBAnyVar(x);
} error:^(NSError *error) {

LxDBAnyVar(error);
} completed:^{

LxPrintAnything(completed);
}]
;

上面代码的意思就是,我等待3秒中发送(afterDelay),但是我超时了(timeout)2秒钟才发送,所以这条信息发生错误,会走error的方法。 这种情况可以用在封装http client中,当然你可能遇到别的需求,也需要它。

take – skip
1
2
3
4
5
6
7
8
9
10
11
12
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"rac1"];
[subscriber sendNext:@"rac2"];
[subscriber sendNext:@"rac3"];
[subscriber sendNext:@"rac4"];
[subscriber sendCompleted];
return nil;
}]take:2];//Skip

[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

比如说我们发送了很多次请求

take表示我们只取前两次
skip表示跳过前两次
takeLast表示倒数的前两次
takeUntil这个值比较特殊,他后面的参数是个信号,它的意思是,当takeUntil发送这个信号的时候,上面的发送信号就会停止发送。

接下来是几个block回调方法

takeWhileBlock BOOL值,意思是当返回YES的时候,订阅者才能收到信号
skipWhileBlock BOOL值,意思是当返回YES的时候,订阅者就会跳过信号,NO的时候才接受
skipUntilBlock BOOL值,意思是 返回NO的时候,不会收到消息, 直到返回YES的时候才开始收消息。

即时搜索优化 (throttle,distinctUntilChanged,ignore)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];
[self.view addSubview:textField];

@weakify(self);

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];
//throttle 后面是个时间 表示rac_textSignal发送消息,0.3秒内没有再次发送就会相应,若是0.3内又发送消息了,便会在新的信息处重新计时
//distinctUntilChanged 表示两个消息相同的时候,只会发送一个请求
//ignore 表示如果消息和ignore后面的消息相同,则会忽略掉这条消息,不让其发送
[[[[[[textField.rac_textSignal throttle:0.3] distinctUntilChanged] ignore:@""] map:^id(id value) {

return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// network request
[subscriber sendNext:value];
[subscriber sendCompleted];

return [RACDisposable disposableWithBlock:^{

// cancel request
}];
}];
}]switchToLatest] subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

以上代码,是用textField模拟一个即时搜索优化的功能,其中参数如下:

throttle 后面是个时间 表示rac_textSignal发送消息,0.3秒内没有再次发送就会相应,若是0.3内又发送消息了,便会在新的信息处重新计时
distinctUntilChanged 表示两个消息相同的时候,只会发送一个请求
ignore 表示如果消息和ignore后面的消息相同,则会忽略掉这条消息,不让其发送

这样做,是不是给服务器减小了很多的压力,更是节省了我们大量的代码。 其中我们用map建立了一个新的信号,我们知道textField的改变是一个信号, map就是在这个信号上,又加了一个信号,即signal of signals
订阅者所打印的消息x则是,map发出的信号。我们可以再map中发送新的信号,以及取消信号disposable.
当我们用map发送信号的时候,我们则需要使用 switchToLatest这个参数来获取最后一个信号,也就是我们最后所打印的x,就是map最后发错的这个信号。

repeat
1
2
3
4
5
6
7
8
9
10
11
12
13
[[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

[subscriber sendNext:@"rac"];
[subscriber sendCompleted];

return nil;
}]
delay:1]
repeat]
take:3]
subscribeNext:^(id x) {

LxDBAnyVar(x);
} completed:^{

LxPrintAnything(completed);
}]
;

repeat,顾名思义,就是重复发送这条消息,当我们在后面添加了delay和take的时候,意思就是每隔1秒发送一次这条消息,发送3次后停止。

merge – concat – zipWith
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
RACSignal * signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
LxPrintAnything(a);
[subscriber sendNext:@"a"];
[subscriber sendCompleted];
});

return nil;
}];

RACSignal * signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
LxPrintAnything(b);
[subscriber sendNext:@"b"];
[subscriber sendCompleted];
});

return nil;
}];

[[RACSignal merge:@[signalA, signalB]]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

我们创建了两个请求,A和B,用GCD的方法A延迟两秒钟,B延迟了3秒钟,我们用merge方法合并了A和B,打印结果为

1
2
3
4
__23-[ViewController merge]_block_invoke_2 + 66 a
__23-[ViewController merge]_block_invoke29 + 87 x = a
__23-[ViewController merge]_block_invoke_215 + 77 b
__23-[ViewController merge]_block_invoke29 + 87 x = b

也就是A和B不管谁发送都会打印x,简单的说就是A和B的打印方法用的是同一个。他们之间关系是独立的,如果A发送失败,B依然会执行。

当我们用concat方法链接A和B之后,意思就是当A执行完了之后才会执行B,他们之间是依赖的关系,如果A发送失败,B也不会执行。

请注意合并(merge)和链接(concat)的区别。

zipWith,当用zipWith链接A和B的时候,只有在A.B每隔都至少发送过一次消息的时候才会执行zipWith的方法,它的返回值是一个集合,也就是数组,同时包含了A和B的打印结果。
zipWith的写法等同于 :

1
2
3
4
[[RACSignal combineLatestWith:@[signalA, signalB]subscribeNext:^(id x) {

LxDBAnyVar(x);
}]
;

亦或者

1
2
3
4
[[RACSignal combineLatest:@[signalA, signalB]]subscribeNext:^(id x) {

LxDBAnyVar(x);
}]
;

但是使用combineLatest,可以再后面添加更多的信号.

RAC(<#TARGET, …#>) 宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//button setBackgroundColor:forState:

UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
[self.view addSubview:button];

@weakify(self);

[button mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];


RAC(button, backgroundColor) = [RACObserve(button, selected) map:^UIColor *(NSNumber * selected) {

return [selected boolValue] [UIColor redColor] : [UIColor greenColor];
}];

[[button rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(UIButton * btn) {

btn.selected = !btn.selected;
}];

比如Btn的设置背景颜色的属性,OC中并没有button setBackgroundColor:forState:这种方法,我们不能直接设置其选中后的颜色。在RAC中,则可以很简单的改变BTN的背景颜色。不得不说RAC的简单和强大。

做一个秒表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UILabel * label = ({

UILabel * label = [[UILabel alloc]init];
label.backgroundColor = [UIColor cyanColor];
label;
});
[self.view addSubview:label];

@weakify(self);

[label mas_makeConstraints:^(MASConstraintMaker *make) {
@strongify(self);

make.size.mas_equalTo(CGSizeMake(240, 40));
make.center.equalTo(self.view);

}];

RAC(label, text) = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] map:^NSString *(NSDate * date) {

return date.description;
}];

只有这么多代码,我们便可以完美的做一个秒表,是否很cool?

结束

当我们大量使用RAC写代码的时候,会把一个个事件封装成一个个信号,通过触发信号,订阅这个信号来返回各种信息。RAC使我们的代码耦合性根底,聚合性更高。

若有不懂得地方可以留言,若有写错的地方,请及时与我联系,可以留言或者Email等。

文本所用的Demo,下载地址 戳这里.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多