下面我来逐一介绍ReactiveCocoa框架的每个组件
Streams 表现为RACStream类,可以看做是水管里面流动的一系列玻璃球,它们有顺序的依次通过,在第一个玻璃球没有到达之前,你没法获得第二个玻璃球。
RACStream描述的就是这种线性流动玻璃球的形态,比较抽象,它本身的使用意义并不很大,一般会以signals或者sequences等这些更高层次的表现形态代替。
A stream is an abstraction for a series of values. You can think of a stream like a pipe, where values are inserted at one end and come out the other. It’s impossible to access past values, or even the current value unless you’re at the end of the pipe when it comes out. That’s OK, as we’ll see.
A series of values, eh? Kind of like a list, or in our case, an array. In fact, we can easily turn an NSArray into a stream with the rac_sequence method.
NSArray *array = @[@(1), @(2), @(3)];
RACSequence *stream = [array rac_sequence];
Wait. Sequences? I thought we were dealing with streams?
Well, we are. A sequence is one of two specific types of streams. In fact, RACSequence is
a subclass of RACStream.
At any rate, what do we do with our stream? Well, I’dlike to show you how we can apply the same methods from the last chapter onthis new stream. Let’s apply our same squaring map.
[stream map:^id(id value) {
return @(pow([value integerValue], 2));
}];
Note: LikeNSArray, streams cannot contain nil.
That’s great, but map just returns a stream. How would we get that back to an array? Well, luckily RACSequence has this handy method called array.
NSLog(@"%@", [stream array]);
Of course, we can combine the above methodinvocations so we’re not polluting our local variable scope.
NSLog(@"%@", [[[array rac_sequence] map:^id(id value) {
return @(pow([value integerValue], 2));
}] array]);
To recap, we’ve got an array, which is a list of values. Then we’re turning that array into a sequence, which is a kind of stream. We’re mapping that sequence, which returns a new sequence, then turning it back into an array.
reduceEach的作用是传入多个参数,返回单个参数,是基于`map`的一种实现。示例:
reduceEach: ^id(NSURLResponse *response, NSData *data){
return data;
}]
sequence 表现为RACSequence类,可以简单看做是RAC世界的NSArray,RAC增加了-rac_sequence方法,可以使诸如NSArray这些集合类(collection classes)直接转换为RACSequence来使用。
Sequences are, by default, lazily-loaded.They’re pull-driven, providing values whenever they’re asked for them. Thearray method forces evaluationof each of the members of the sequence.
Let’s take a look at filtering. In order to filter our array using ReactiveCocoa, we need to again turn it into a sequence to filter it.
NSLog(@"%@", [[[array rac_sequence] filter:^BOOL(id value) {
return [value integerValue] % 2 == 0;
}] array]);
Finally, we can see how to to fold values from a sequence down to a single value.
NSLog(@"%@", [[[array rac_sequence] map:^id(id value) {
return [valuestringValue];
}] foldLeftWithStart:@"" reduce: ^id(id accumulator, id value) {
return [accumulator stringByAppendingString: value];
}]);
In this case, we’re chaining two operationson sequences together. This is going to become a key concept when we discusssignals in the next section.
ReactiveCocoa has a concept of a left fold and a right fold. A left fold will traverse an array from its beginning to its end, where a right fold will traverse it backward. The nomenclature is an allusion to programming languages with list comprehension, which Objective-C does not have.
A signal is another type of stream. In contrast to sequences, signals are push-driven. New values are pushed through the pipe and cannot be pulled. They abstract data that will be delivered in the future.
Signals send three different types of values. Next values are exactly whatthe appear to be: the next value sent down the pipe. Error values indicate thata signal could not complete successfully. They are relatively rare, and we’ll see in the next chapter how to use them. Completion values indicate that a signal completed successfully.
It’s important to note that once an error or completion value is sent viaa signal, no other values will be sent. Additionally, only one of either error or completion will be sent – never both.
Signals are one of the core components of ReactiveCocoa. There are signal selectors built-in to UIKit components. For example, UITextField has a rac_textSignal whose values are sent with every new key press in a text field.
Class diagram
Signals can also be chained andtransformed. When we map or filter a stream, we create a new stream. Thisstream can subsequently be mapped, filtered, and fiddled with all we like.
Signals 表现为RACSignal类,就是前面提到水龙头,ReactiveCocoa的核心概念就是Signal,它一般表示未来要到达的值,想象玻璃球一个个从水龙头里出来,只有了接收方(subscriber)才能获取到这些玻璃球(value)。
Signal会发送下面三种事件给它的接受方(subscriber),想象成水龙头有个指示灯来汇报它的工作状态,接受方通过-subscribeNext:error:completed:对不同事件作出相应反应
• next 从水龙头里流出的新玻璃球(value);
• error 获取新的玻璃球发生了错误,一般要发送一个NSError对象,表明哪里错了;
• completed 全部玻璃球已经顺利抵达,没有更多的玻璃球加入了;
一个生命周期的Signal可以发送任意多个“next”事件,和一个“error”或者“completed”事件(当然“error”和“completed”只可能出现一种)。
Signal是RAC的核心,为了帮助理解,画了这张简化图
这里的数据源和sendXXX,可以理解为函数的参数和返回值。当Signal处理完数据后,可以向下一个Signal或Subscriber传送数据。可以看到上半部分的两个Signal是冷的(cold),相当于实现了某个函数,但该函数没有被调用。同时也说明了Signal可以被组合使用,比如RACSignal *signalB = [signalA map: ^id(id x){return x}],或RACSignal *signalB = [signalA take:1]等等。
当signal被subscribe时,就会处于热(hot)的状态,也就是该函数会被执行。比如上面的第二张图,首先signalA可能发了一个网络请求,拿到结果后,把数据通过sendNext方法传递到下一个signal,signalB可以根据需要做进一步处理,比如转换成相应的Model,转换完后再sendNext到subscriber,subscriber拿到数据后,再改变ViewModel,同时因为View已经绑定了ViewModel,所以拿到的数据会自动在View里呈现。
还有,一个signal可以被多个subscriber订阅,这里怕显得太乱就没有画出来,但每次被新的subscriber订阅时,都会导致数据源的处理逻辑被触发一次,这很有可能导致意想不到的结果,需要注意一下。
当数据从signal传送到subscriber时,还可以通过doXXX来做点事情,比如打印数据。
通过这张图可以看到,这非常像中学时学的函数,比如 f(x) = y,某一个函数的输出又可以作为另一个函数的输入,比如f(f(x)) = z,这也正是「函数响应式编程」(FRP)的核心。
有些地方需要注意下,比如把signal作为local变量时,如果没有被subscribe,那么方法执行完后,该变量会被dealloc。但如果signal有被subscribe,那么subscriber会持有该signal,直到signal
sendCompleted或sendError时,才会解除持有关系,signal才会被dealloc。
Subscriptions are made on streams – most often signals – when you want tobe notified that a new value is sent (either next, error, or completion).Signals are often used to have side effects, such as the following example.
Let’s add a text field to our view controller’s view and connect it to an IBOutlet. I’m going to do this using the built-in Storyboard and Assistant Editor, but you do whatever you like.
Adding a text field
Add the following lines of code to viewDidLoad. They subscribe to the rac_textSignal of our new text field.
[self.textField.rac_textSignal subscribeNext: ^ (id x) {
NSLog(@"New value: %@", x);
} error: ^ (NSError *error) {
NSLog(@"Error: %@", error);
} completed:^{
NSLog(@"Completed.");
}];
Build and run the application and type something into the text field. Every time a new value is entered into the text field, the next value is sent down the pipe. Then, our subscription block was executed for each new value.
Interestingly, this particular signal doesn’t typically send error values and only sends a completion value when it’s deallocated, so those subscription blocks will not be called often or at all. We can simplify our code by using a convenience method on RACSignal, subscribeNext:.
[self.textField.rac_textSignal subscribeNext: ^(id x) {
NSLog(@"New value: %@", x);
}];
Note. A lot less code.
When you subscribe to a signal, you actually create a subscription object. This object is automatically retained for you, and retains the signal its subscribing to, as well. You can manually dispose of the subscriber, if you want, but this is not typical behavior. We’ll see in the next chapter how disposing of signals can be helpful when used with reused views (such as collection view or table view cells).
Deriving state is another one of the core components of ReactiveCocoa.Rather than have a property on a class that is set to new values as the state changes, we can abstract that property into a stream. Let’s take the previous example and augment it with derived state.
Let’s pretend that our view is a form forcreating some account and we only want to allow email addresses that contain an‘@’ symbol. When, and only when, a valid user name has been entered, then abutton’s enabled state will beYES. We also want to provide feedback to the user in way of the text colourof the text field.
Let’s first add that button to our view and create an IBOutlet for it.
Added a button
Next, we’ll want to bind our button’s enabled property to asignal that we’ll create.
RAC(self.button, enabled) = [self.textField.rac_textSignal map: ^id(NSString *value) {
return @([value rangeOfString: @"@"].location != NSNotFound);
}];
Note that
we’ll see later how we can use commands with buttons to better bind its enabled property.
The RAC()macro takes two arguments: an object and a key path of that object. It then performs a one-way binding of the right-hand value of the expression to the key path in question. Values must be NSObjects, which is why we wrap our boolean expression in an NSNumber.
But what about the text color? We can actually re-use the signal from earlier with a little bit of refactoring.
RACSignal *validEmailSignal = [self.textField.rac_textSignal map: ^id(NSString *value) {
return @([value rangeOfString: @"@"].location != NSNotFound);
}];
RAC(self.button, enabled) =validEmailSignal;
RAC(self.textField, textColor) = [validEmailSignal map: ^id(id value) {
if([value boolValue]) {
return [UIColor greenColor];
}
else{
return [UIColor redColor];
}
}];
Great! See how we’re re-using the validEmailSignal? That’s a very common pattern in Reactive-Cocoa. We’re also not writing any code outside of our viewDidLoad method, which is also very common.
We mentioned in the last section that binding the enabled property of aUIButton was not the best idea. That’s because UIButton has been augmented by a ReactiveCocoa category, adding a command.
We’ll talk about what commands are in this section. The important thing to note is that a button’s rac_command property takes care of the enabled state for us.
Quoting from the ReactiveCocoa documentation:
A command, represented by the RACCommand class,creates and subscribes to a
signal in response to some action.This makes it easy to perform side-effecting work as the user
interacts with the app.
Usually the action triggering a command is UI-driven, like when a button is clicked. Commands can also be automatically disabled based on a signal, and this disabled state can be represented in a UI by disabling any controls associated with the command.
Commands are useful when you want a signal value sent in response to a user interaction event. The command’s signal can be subscribed to later on to process the output of the signal that we return. It’s a little confusing, but we’re going to see how to use commands in practice in Chapter 5. For now, replace the binding of the enabled property on our button to the following.
self.button.rac_command = [[RACCommand alloc] initWithEnabled: validEmailSignal signalBlock:^ RACSignal * (id input){
NSLog(@"Button was pressed.");
return [RACSignal empty];
}];
The signal block is executed whenever the button is pressed, and the rac_command property takes care of binding the enabled signal to the enabled state of the button (in fact, if we had kept our original code, we would have caused an error with two bindings to the same property).
But what about the return value? Well, we need to return a signal that will be sent down the executionSignals pipe belonging to the RACCommand. This lets you return a signal representing some work that will need to be done as the result of the button press. The button will remain disabled until that signal returns its complete value (empty returns this value immediately). Because we’re simply logging the result of the button press, we’re returning an empty signal in this case.
We’ll return to RACCommand and its uses in chapter 5.
command表现为RACCommand类,偷个懒直接举个例子吧,比如一个简单的注册界面:
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];
subjects 表现为RACSubject类,可以认为是“可变的(mutable)”信号/自定义信号,它是嫁接非RAC代码到Signals世界的桥梁,很有用。嗯。。。 这样讲还是很抽象,举个例子吧:
RACSubject *letters = [RACSubject subject];
RACSignal *signal = [letters sendNext: @"a"];
可以看到@"a"只是一个NSString对象,要想在水管里顺利流动,就要借RACSubject的力。
RACReplaySubject* subject = [RACReplaySubject subject];
[self downloadFullsizedImageForPhotoModel: photoModel];
[subject sendNext:photoModel];
[subject sendCompleted];
scheduler 表现为RACScheduler类,类似于GCD,but schedulers support cancellation, and always execute serially.
使用实例:
+ (RACSignal *)download:(NSString *)urlString {
NSAssert(urlString, @"URL must not be nil");
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString: urlString]];
return [[[NSURLConnection rac_sendAsynchronousRequest: request] map: ^id(RACTuple*value) {
return [value second];
}] deliverOn: [RACScheduler mainThreadScheduler]];
A tuple is an ordered collection of objects. It may contain nils, represented by RACTupleNil.
使用实例:
+(RACSignal *)download:(NSString *)urlString {
NSAssert(urlString, @"URL must not be nil");
NSURLRequest *request = [NSURLRequest requestWithURL: [NSURL URLWithString: urlString]];
return [[[NSURLConnection rac_sendAsynchronousRequest: request] map: ^id(RACTuple*value) {
return [value second];
}] deliverOn: [RACScheduler mainThreadScheduler]];
}
RACMulitcastConnection will subscribe to the receiving signal when it’s connected. That’s what autoconnect() does for us: connects to the underly signal when the signal it returns is subscribed to.
实践出真知,下面就举一些简单的例子,一起看看RAC的使用
接收 -subscribeNext: -subscribeError: -subscribeCompleted:
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);
}];
如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。
注入效果 -doNext: -doError: -doCompleted:,看下面注释应该就明白了:
__block unsigned subscriptions=0;
RACSignal *loggingSignal=[RACSignal createSignal: ^RACDisposable *(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);
}];
-map: 映射,可以看做对玻璃球的变换、重新组装。
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];
}];
-filter: 过滤,不符合要求的玻璃球不允许通过。
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;
}];
-concat: 把一个水管拼接到另一个水管之后。
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];
-flatten: Sequences are concatenated
代码:
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可以理解成把几个水管的龙头合并成一个,哪个水管中的玻璃球哪个先到先吐哪个玻璃球)
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"];
-flattenMap: 先 map 再 flatten
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.");
}];
-then:
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;
}];
+merge: 前面在flatten中提到的水龙头的合并。
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"];
+combineLatest: 任何时刻取每个水龙头吐出的最新的那个玻璃球。
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"];
-switchToLatest: 取指定的那个水龙头的吐出的最新玻璃球。
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"];
使用实例:
+ (RACSignal *) download: (NSString *)urlString {
NSAssert(urlString, @"URL must not be nil");
NSURLRequest *request = [NSURLRequest requestWithURL: [NSURL URLWithString: urlString]];
return [[[NSURLConnection rac_sendAsynchronousRequest: request] map: ^id(RACTuple*value) {
return [value second];
}] deliverOn: [RACScheduler mainThreadScheduler]];
//简写
+ (RACSignal *) fetchPhotoDetails: (FRPPhotoModel*) photoModel
{
NSURLRequest *request = [self photoURLRequest: photoModel];
return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request] map: ^id(RACTuple*value) {
return [value second];
}] deliverOn: [RACScheduler mainThreadScheduler]] map: ^id(NSData*data) {
id results = [NSJSONSerialization JSONObjectWithData: data options:0 error: nil] [@"photo"];
[self configurePhotoModel: photoModel withDictionary: results];
[self downloadFullsizedImageForPhotoModel: photoModel];
return photoModel;
}] publish] autoconnect];
}
RAC 可以看作某个属性的值与一些信号的联动。
- RAC(TARGET, KEYPATH, NILVALUE) will bind the `KEYPATH` of `TARGET` to the given signal. If the signal ever sends a `nil` value, the property will be set to `NILVALUE` instead. `NILVALUE` may itself be `nil` for object properties, but an NSValue should be used for primitive properties, to avoid an
exception if `nil` is sent (which might occur if an intermediate object is set to `nil`).
- RAC(TARGET, KEYPATH) is the same as the above, but `NILVALUE` defaults to `nil`.
示例一:
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);
}];
示例二:
RACSignal *photoSignal = [FRPPhotoImporter importPhotos];
RACSignal *photosLoaded = [photoSignal catch: ^RACSignal *(NSError*error) {
NSLog(@"Couldn't fetch photos from 500px: %@", error);
return [RACSignal empty];
}];
//将photosArray的变化绑定到self.collectionView
RAC(self, photosArray) = photosLoaded;
[photosLoaded subscribeCompleted: ^{
@strongify(self);
[self.collectionView reloadData];
}];
示例三:
RAC(self, photosArray) = [[[[FRPPhotoImporter importPhotos]
doCompleted: ^{
@strongify(self);
[self.collectionView reloadData];
}] logError] catchTo: [RACSignal empty]];
RACObserve 监听属性的改变,使用block的KVO
示例一:
[RACObserve(self.textField,text) subscribeNext: ^(NSString *newName)
{
NSLog(@"%@",newName);
}];
示例二:
RAC(self.imageView, image) = [[RACObserve(self, photoModel.thumbnailData) ignore: nil] map:^(NSData*data) {
return [UIImage imageWithData: data];
}];
上面提到过这两个概念,冷信号默认什么也不干,比如下面这段代码
RACSignal *signal = [RACSignal createSignal: ^ RACDisposable * (id subscriber) {
NSLog(@"triggered");
[subscriber sendNext: @"foobar"];
[subscriber sendCompleted];
return nil;
}];
我们创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。加了下面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。
[signal subscribeCompleted: ^{
NSLog(@"subscription %u", subscriptions);
}];
或许你会问,那如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect。
RAC为系统UI提供了很多category,非常棒,比如UITextView、UITextField文本框的改动rac_textSignal,UIButton的的按下rac_command等等。
上面看到的rac_textSignal是加在UITextField上的(UITextField+RACSignalSupport.h),其他常用的UIView也都有添加相应的category,比如UIAlertView,就不需要再用Delegate了。
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle: @"" message: @"Alert" delegate: nil cancelButtonTitle: @"YES" otherButtonTitles: @"NO", nil];
[[alertView rac_buttonClickedSignal] subscribeNext: ^(NSNumber *indexNumber) {
if ([indexNumber intValue] == 1) {
NSLog(@"you touched NO");
}
else{
NSLog(@"you touched YES");
}
}];
[alertView show];
有了这些Category,大部分的Delegate都可以使用RAC来做。或许你会想,可不可以subscribe NSMutableArray.rac_sequence.signal,这样每次有新的object或旧的object被移除时都能知道,UITableViewController就可以根据dataSource的变化,来reloadData。但很可惜这样不行,因为RAC是基于KVO的,而NSMutableArray并不会在调用addObject或removeObject时发送通知,所以不可行。不过可以使用NSArray作为UITableView的dataSource,只要dataSource有变动就换成新的Array,这样就可以了。
说到UITableView,再说一下UITableViewCell,RAC给UITableViewCell提供了一个方法:rac_prepareForReuseSignal,它的作用是当Cell即将要被重用时,告诉Cell。想象Cell上有多个button,Cell在初始化时给每个button都addTarget: action: forControlEvents,被重用时需要先移除这些target,下面这段代码就可以很方便地解决这个问题:
[[[self.cancelButton rac_signalForControlEvents: UIControlEventTouchUpInside] takeUntil: self.rac_prepareForReuseSignal] subscribeNext: ^(UIButton *x) {
// do other things
}];
还有一个很常用的category就是UIButton+RACCommandSupport.h,它提供了一个property:rac_command,就是当button被按下时会执行的一个命令,命令被执行完后可以返回一个signal,有了signal就有了灵活性。比如点击投票按钮,先判断一下有没有登录,如果有就发HTTP请求,没有就弹出登陆框,可以这么实现。
voteButton.rac_command = [[RACCommand alloc] initWithEnabled: self.viewModel.voteCommand.enabled signalBlock: ^RACSignal *(id input) {
// Assume that we're logged in at first. We'll replace this signal later if not.
RACSignal *authSignal = [RACSignal empty];
if ([[PXRequest apiHelper] authMode] == PXAPIHelperModeNoAuth) {
// Not logged in. Replace signal.
authSignal = [[RACSignal createSignal: ^RACDisposable *(id subscriber) {
@strongify(self);
FRPLoginViewController *viewController = [[FRPLoginViewController alloc] initWithNibName: @"FRPLoginViewController" bundle: nil];
UINavigationController* navigationController = [[UINavigationController alloc] initWithRootViewController: viewController];
[self presentViewController: navigationController animated: YES completion: ^{
[subscriber sendCompleted];
}];
return nil;
}]];
}
return [authSignal then: ^RACSignal *{
@strongify(self);
return [[self.viewModel.voteCommand execute: nil] ignoreValues];
}];
}];
[voteButton.rac_command.errors subscribeNext: ^(id x){
[x subscribeNext: ^(NSError *error) {
[SVProgressHUD showErrorWithStatus: [error localizedDescription]];
}];
}];
这段代码节选自AshFurrow的FunctionalReactivePixels,有删减。
常用的数据结构,如NSArray, NSDictionary也都有添加相应的category,比如NSArray添加了rac_sequence,可以将NSArray转换为RACSequence,顺便说一下RACSequence, RACSequence是一组immutable且有序的values,不过这些values是运行时计算的,所以对性能提升有一定的帮助。RACSequence提供了一些方法,如array转换为NSArray,any:检查是否有Value符合要求,all:检查是不是所有的value都符合要求,这里的符合要求的,block返回YES,不符合要求的就返回NO。
NSNotificationCenter, 默认情况下NSNotificationCenter使用Target-Action方式来处理Notification,这样就需要另外定义一个方法,这就涉及到编程领域的两大难题之一:起名字。有了RAC,就有Signal,有了Signal就可以subscribe,于是NotificationCenter就可以这么来处理,还不用担心移除observer的问题。
[[[NSNotificationCenter defaultCenter] rac_addObserverForName: @"MyNotification" object: nil] subscribeNext: ^(NSNotification *notification) {
NSLog(@"Notification Received");
}];
NSObject有不少的Category,我觉得比较有用的有这么几个
顾名思义就是在一个object的dealloc被触发时,执行的一段代码。
NSArray *array = @[@"foo"];
[[array rac_willDeallocSignal] subscribeCompleted: ^{
NSLog(@"oops, i will be gone");
}];
array = nil;
有时我们希望满足一定条件时,自动触发某个方法,有了这个category就可以这么办。
- (void) test{
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id subscriber) {
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[subscriber sendNext: @"A"];
});
return nil;
}];
RACSignal *signalB = [RACSignal createSignal: ^RACDisposable *(id subscriber) {
[subscriber sendNext: @"B"];
[subscriber sendNext: @"Another B"];
[subscriber sendCompleted];
return nil;
}];
[self rac_liftSelector: @selector(doA: withB:) withSignals: signalA, signalB, nil];
}
- (void) doA: (NSString *)A withB: (NSString *)B{
NSLog(@"A:%@ and B:%@", A, B);
}
这里的rac_liftSelector:withSignals 就是干这件事的,它的意思是当signalA和signalB都至少sendNext过一次,接下来只要其中任意一个signal有了新的内容,doA:withB这个方法就会自动被触发。
如果你有兴趣,可以想想上面这段代码会输出什么。
这个category有rac_signalForSelector:和rac_signalForSelector:fromProtocol: 这两个方法。先来看前一个,它的意思是当某个selector被调用时,再执行一段指定的代码,相当于hook。比如点击某个按钮后,记个日志。后者表示该selector实现了某个协议,所以可以用它来实现Delegate。
例1. 监听对象的成员变量变化,当成员变量值被改变时,触发做一些事情。
这种情况其实就是IOS KVO机制使用的场景,使用KVO实现,通常有三个步骤:1,给对象的成员变量添加监听;2,实现监听回调;3,取消监听;而通过RAC可以直接实现,RAC的回调是通过block实现的,类似于过程式编程,上下文也更容易理解一些。
场景:当前类有一个成员变量 NSString *input,当它的值被改变时,发送一个请求。
实现:
[RACObserve(self, input) subscribeNext: ^(NSString* x){
request(x);//发送一个请求
}];
每次input值被修改时,就会调用此block,并且把修改后的值做为参数传进来。
场景:在上面场景中,当用户输入的值以2开头时,才发请求.
实现:
[[RACObserve(self, input) filter: ^(NSString* value){
if ([value hasPrefix: @"2"]) {
return YES;
} else{
return NO;
}
}] subscribeNext: ^(NSString* x){
request(x);//发送一个请求
}];
场景:上面场景是监听自己的成员变量,如果想监听UITextField输入值变化,框架也做了封装可以代替系统回调。
实现:
[[self.priceInput.rac_textSignal filter: ^(NSString *str) {
if (str.integerValue > 20) {
return YES;
} else{
return NO;
}
}] subscribeNext: ^(NSString *str) {
request(x);//发送一个请求
}];
例2. 同时监听多个变量变化,当这些变量满足一定条件时,使button为可点击状态。
场景:button监听 两个输入框有值和一个成员变量值,当输入框有输入且成员变量为真时,button为可点击状态。
实现:
RAC(self.payButton, enabled) = [RACSignal combineLatest: @[self.priceInput.rac_textSignal, self.nameInput.rac_textSignal, RACObserve(self, isConnected)] reduce: ^(NSString *price, NSString *name, NSNumber*connect){
return @(price.length > 0 && name.length > 0 && [connect boolValue]);
}];
场景:满足上面条件时,直接发送请求
实现:
[[RACSignalcombineLatest: @[self.priceInput.rac_textSignal, self.nameInput.rac_textSignal, RACObserve(self, isConnected)] reduce:^(NSString *price, NSString *name, NSNumber *connect){
return @(price.length > 0 && name.length > 0 && ![connect boolValue]);
}] subscribeNext: ^(NSNumber *res){
if ([res boolValue]) {
NSLog(@"XXXXX send request");
}
}];
例3. 类似于生产-消费
场景:用户每次在TextField中输入一个字符,1秒内没有其它输入时,去发一个请求。TextField中字符改变触发事件已在例1中展示,这里实现一下它触法的方法,把1秒延时在此方法中实现。
实现:
- (void) showLoading {
[self.loadingDispose dispose];//上次信号还没处理,取消它(距离上次生成还不到1秒)
@weakify(self);
self.loadingDispose = [[[RACSignal createSignal: ^RACDisposable *(id subscriber) {
[subscriber sendCompleted];
return nil;
}] delay:1] //延时一秒
subscribeCompleted: ^{
@strongify(self);
doRequest();
self.loadingDispose = nil;
}];
}
上面代码看起来挻费解,不过下面一段类似的代码拆开写的,会比较容易理解:
[self.loadingDispose dispose];
RACSignal *loggingSignal = [RACSignal createSignal: ^RACDisposable * (id subscriber) {
//BLOCK_1
subscriptions++;
[subscriber sendNext: @"mytest"];
[subscriber sendCompleted];
return nil;
}];
loggingSignal = [loggingSignal delay: 10];
self.loadingDispose = [loggingSignal subscribeNext: ^(NSString* x){
//BLOCK_2
NSLog(@"%@", x);
NSLog(@"subscription %u", subscriptions);
}];
self.loadingDispose = [loggingSignal subscribeCompleted: ^{
//BLOCK_3
NSLog(@"subscription %u", subscriptions);
}];
loggingSignal在每次被调用subscriibeNext:^(id x)或subscribeCompleted:^方法时(12行和17行),它创建进传进的参数block_1就会被触动发,而block_1中的sendNext:方法会调用subscriibeNext:^中对应的block_2, 而block_1中的sendCompleted会调用subscribeCompleted:中对应的block_3
iOS开发中有着各种消息传递机制,包括KVO、Notification、delegation、block以及target-action方式。各种消息传递机制使得开发者在做具体选择时感到困惑,例如在objc.io上就有专门撰文(破船的翻译 ),介绍各种消息传递机制之间的差异性。
RAC将传统的UI控件事件进行了封装,使得以上各种消息传递机制都可以用RAC来完成。示例代码如下:
// KVO
[RACObserve(self, username) subscribeNext: ^(id x) {
NSLog(@"成员变量 username 被修改成了:%@", x);
}];
// target-action
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock: ^RACSignal *(id input) {
NSLog(@"按钮被点击");
return [RACSignal empty];
}];
// Notification
[NSNotificationCenter.defaultCenter addObserver:self
selector: @selector(keyboardDidChangeFrameNotificationHandler:)
name: UIKeyboardDidChangeFrameNotification object: nil];
RAC的RACSignal 类也提供了createSignal方法来让用户创建自定义的信号,如下代码创建了一个下载指定网站内容的信号。
(RACSignal *)urlResults {
return [RACSignalcreateSignal: ^RACDisposable *(id subscriber) {
NSError *error;
NSString *result = [NSString stringWithContentsOfURL: [NSURL URLWithString: @"http://www.devtang.com"] encoding: NSUTF8StringEncoding error: &error]; NSLog(@"download");
if (!result) {
[subscriber sendError: error];
}
else {
[subscriber sendNext: result];
[subscriber sendCompleted];
}
return [RACDisposable disposableWithBlock:^{
NSLog(@"clean up");
}];
}];
}