首页 > 代码库 > 使用ReactiveCocoa实现iOS平台响应式编程

使用ReactiveCocoa实现iOS平台响应式编程

使用ReactiveCocoa实现iOS平台响应式编程

ReactiveCocoa和响应式编程

在说ReactiveCocoa之前,先要介绍一下FRP(Functional Reactive Programming,响应式编程),在维基百科中有这样一个例子介绍:

在命令式编程环境中,a = b + c 表示将表达式的结果赋给a,而之后改变b或c的值不会影响a。但在响应式编程中,a的值会随着b或c的更新而更新。

Excel就是响应式编程的一个例子。单元格可以包含字面值或类似”=B1+C1″的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。

而ReactiveCocoa简称RAC,就是基于响应式编程思想的Objective-C实践,它是Github的一个开源项目,你可以在这里找到它。

关于FRP和ReactiveCocoa可以去看leezhong的这篇blog,图文并茂,讲的很好。

ReactiveCocoa框架概览

先来看一下leezhong再博文中提到的比喻,让你对有个ReactiveCocoa很好的理解:

可以把信号想象成水龙头,只不过里面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。这样只要有新的玻璃球进来,就会自动传送给接收方。可以在水龙头上加一个过滤嘴(filter),不符合的不让通过,也可以加一个改动装置,把球改变成符合自己的需求(map)。也可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),这样只要其中的一个水龙头有玻璃球出来,这个新合并的水龙头就会得到这个球。

下面我来逐一介绍ReactiveCocoa框架的每个组件

Streams

Streams 表现为RACStream类,可以看做是水管里面流动的一系列玻璃球,它们有顺序的依次通过,在第一个玻璃球没有到达之前,你没法获得第二个玻璃球。
RACStream描述的就是这种线性流动玻璃球的形态,比较抽象,它本身的使用意义并不很大,一般会以signals或者sequences等这些更高层次的表现形态代替。

Signals

Signals 表现为RACSignal类,就是前面提到水龙头,ReactiveCocoa的核心概念就是Signal,它一般表示未来要到达的值,想象玻璃球一个个从水龙头里出来,只有了接收方(subscriber)才能获取到这些玻璃球(value)。

Signal会发送下面三种事件给它的接受方(subscriber),想象成水龙头有个指示灯来汇报它的工作状态,接受方通过-subscribeNext:error:completed:对不同事件作出相应反应

  • next 从水龙头里流出的新玻璃球(value)

  • error 获取新的玻璃球发生了错误,一般要发送一个NSError对象,表明哪里错了

  • completed 全部玻璃球已经顺利抵达,没有更多的玻璃球加入了

一个生命周期的Signal可以发送任意多个“next”事件,和一个“error”或者“completed”事件(当然“error”和“completed”只可能出现一种)

Subjects

subjects 表现为RACSubject类,可以认为是“可变的(mutable)”信号/自定义信号,它是嫁接非RAC代码到Signals世界的桥梁,很有用。嗯。。。 这样讲还是很抽象,举个例子吧:

1

2

3

RACSubject *letters = [RACSubject subject];

RACSignal *signal = [letters sendNext:@"a"];

 

可以看到@"a"只是一个NSString对象,要想在水管里顺利流动,就要借RACSubject的力。

Commands

command 表现为RACCommand类,偷个懒直接举个例子吧,比如一个简单的注册界面:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

    RACSignal *formValid = [RACSignal

        combineLatest:@[

            self.userNameField.rac_textSignal,

            self.emailField.rac_textSignal,

        ]

        reduce:^(NSString *userName, NSString *email) {

            return @(userName.length > 0

                    && email.length > 0);

        }];

 

   RACCommand *createAccountCommand = [RACCommand commandWithCanExecuteSignal:formValid];

   RACSignal *networkResults = [[[createAccountCommand

       addSignalBlock:^RACSignal *(id value) {

           //... 网络交互代码

       }]

       switchToLatest]

       deliverOn:[RACScheduler mainThreadScheduler]];

 

   // 绑定创建按钮的 UI state 和点击事件

    [[self.createButton rac_signalForControlEvents:UIControlEventTouchUpInside] executeCommand:createAccountCommand];

 

Sequences

sequence 表现为RACSequence类,可以简单看做是RAC世界的NSArray,RAC增加了-rac_sequence方法,可以使诸如NSArray这些集合类(collection classes)直接转换为RACSequence来使用。

Schedulers

scheduler 表现为RACScheduler类,类似于GCD,but schedulers support cancellationbut schedulers support cancellation, and always execute serially.

ReactiveCocoa的简单使用

实践出真知,下面就举一些简单的例子,一起看看RAC的使用

Subscription

接收 -subscribeNext: -subscribeError: -subscribeCompleted:

1

2

3

4

5

6

7

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

 

// 依次输出 A B C D…

[letters subscribeNext:^(NSString *x) {

    NSLog(@"%@", x);

}];

 

Injecting effects

注入效果 -doNext: -doError: -doCompleted:,看下面注释应该就明白了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

__block unsigned subscriptions = 0;

 

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

    subscriptions++;

    [subscriber sendCompleted];

    return nil;

}];

 

// 不会输出任何东西

loggingSignal = [loggingSignal doCompleted:^{

    NSLog(@"about to complete subscription %u", subscriptions);

}];

 

// 输出:

// about to complete subscription 1

// subscription 1

[loggingSignal subscribeCompleted:^{

    NSLog(@"subscription %u", subscriptions);

}];

 

Mapping

-map: 映射,可以看做对玻璃球的变换、重新组装

1

2

3

4

5

6

7

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

 

// Contains: AA BB CC DD EE FF GG HH II

RACSequence *mapped = [letters map:^(NSString *value) {

    return [value stringByAppendingString:value];

}];

 

Filtering

-filter: 过滤,不符合要求的玻璃球不允许通过

1

2

3

4

5

6

7

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

 

// Contains: 2 4 6 8

RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {

    return (value.intValue % 2) == 0;

}];

 

Concatenating

-concat: 把一个水管拼接到另一个水管之后

1

2

3

4

5

6

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

 

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9

RACSequence *concatenated = [letters concat:numbers];

 

Flattening

-flatten:

Sequences are concatenated

1

2

3

4

5

6

7

RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;

 

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9

RACSequence *flattened = [sequenceOfSequences flatten];

 

Signals are merged (merge可以理解成把几个水管的龙头合并成一个,哪个水管中的玻璃球哪个先到先吐哪个玻璃球)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

RACSubject *letters = [RACSubject subject];

RACSubject *numbers = [RACSubject subject];

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

    [subscriber sendNext:letters];

    [subscriber sendNext:numbers];

    [subscriber sendCompleted];

    return nil;

}];

 

RACSignal *flattened = [signalOfSignals flatten];

 

// Outputs: A 1 B C 2

[flattened subscribeNext:^(NSString *x) {

    NSLog(@"%@", x);

}];

 

[letters sendNext:@"A"];

[numbers sendNext:@"1"];

[letters sendNext:@"B"];

[letters sendNext:@"C"];

[numbers sendNext:@"2"];

 

Mapping and flattening

-flattenMap: 先 map 再 flatten

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

RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

 

// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

RACSequence *extended = [numbers flattenMap:^(NSString *num) {

    return @[ num, num ].rac_sequence;

}];

 

// Contains: 1_ 3_ 5_ 7_ 9_

RACSequence *edited = [numbers flattenMap:^(NSString *num) {

    if (num.intValue % 2 == 0) {

        return [RACSequence empty];

    } else {

        NSString *newNum = [num stringByAppendingString:@"_"];

        return [RACSequence return:newNum];

    }

}];

 

 

 

 

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

 

[[letters

    flattenMap:^(NSString *letter) {

        return [database saveEntriesForLetter:letter];

    }]

    subscribeCompleted:^{

        NSLog(@"All database entries saved successfully.");

    }];

 

Sequencing

-then:

1

2

3

4

5

6

7

8

9

10

11

12

13

RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

 

// 新水龙头只包含: 1 2 3 4 5 6 7 8 9

//

// 但当有接收时,仍会执行旧水龙头doNext的内容,所以也会输出 A B C D E F G H I

RACSignal *sequenced = [[letters

    doNext:^(NSString *letter) {

        NSLog(@"%@", letter);

    }]

    then:^{

        return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;

    }];

 

Merging

+merge: 前面在flatten中提到的水龙头的合并

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

RACSubject *letters = [RACSubject subject];

RACSubject *numbers = [RACSubject subject];

RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];

 

// Outputs: A 1 B C 2

[merged subscribeNext:^(NSString *x) {

    NSLog(@"%@", x);

}];

 

[letters sendNext:@"A"];

[numbers sendNext:@"1"];

[letters sendNext:@"B"];

[letters sendNext:@"C"];

[numbers sendNext:@"2"];

 

Combining latest values

+combineLatest: 任何时刻取每个水龙头吐出的最新的那个玻璃球

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

RACSubject *letters = [RACSubject subject];

RACSubject *numbers = [RACSubject subject];

RACSignal *combined = [RACSignal

    combineLatest:@[ letters, numbers ]

    reduce:^(NSString *letter, NSString *number) {

        return [letter stringByAppendingString:number];

    }];

 

// Outputs: B1 B2 C2 C3

[combined subscribeNext:^(id x) {

    NSLog(@"%@", x);

}];

 

[letters sendNext:@"A"];

[letters sendNext:@"B"];

[numbers sendNext:@"1"];

[numbers sendNext:@"2"];

[letters sendNext:@"C"];

[numbers sendNext:@"3"];

 

Switching

-switchToLatest: 取指定的那个水龙头的吐出的最新玻璃球

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

RACSubject *letters = [RACSubject subject];

RACSubject *numbers = [RACSubject subject];

RACSubject *signalOfSignals = [RACSubject subject];

 

RACSignal *switched = [signalOfSignals switchToLatest];

 

// Outputs: A B 1 D

[switched subscribeNext:^(NSString *x) {

    NSLog(@"%@", x);

}];

 

[signalOfSignals sendNext:letters];

[letters sendNext:@"A"];

[letters sendNext:@"B"];

 

[signalOfSignals sendNext:numbers];

[letters sendNext:@"C"];

[numbers sendNext:@"1"];

 

[signalOfSignals sendNext:letters];

[numbers sendNext:@"2"];

[letters sendNext:@"D"];

 

常用宏

RAC 可以看作某个属性的值与一些信号的联动

1

2

3

4

RAC(self.submitButton.enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id(NSString *userName, NSString *password) {

    return @(userName.length >= 6 && password.length >= 6);

}];

 

RACObserve 监听属性的改变,使用block的KVO

1

2

3

4

[RACObserve(self.textField, text) subscribeNext:^(NSString *newName) {

     NSLog(@"%@", newName);

}];

 

UI Event

RAC为系统UI提供了很多category,非常棒,比如UITextView、UITextField文本框的改动rac_textSignal,UIButton的的按下rac_command等等。

最后

有了RAC,可以不用去操心值什么时候到达什么时候改变,只需要简单的进行数据来了之后的步骤就可以了。

说了这么多,在回过头去看leezhong的比喻和该文最后总结的关系图,再好好梳理一下吧。我也是初学者,诚惶诚恐的呈上这篇博文,欢迎讨论,如有不正之处欢迎批评指正。

参考

https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/FrameworkOverview.mdhttps://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/BasicOperators.md
http://vimeo.com/65637501
http://iiiyu.com/2013/09/11/learning-ios-notes-twenty-eight/
http://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.html http://nshipster.com/reactivecocoa/