📜 ⬆️ ⬇️

Better world with ReactiveCocoa

Most applications spend a lot of time waiting for events and processing them. They expect user interaction with the interface. Awaiting response to network request. Waiting for completion of asynchronous operations. Waiting for change dependent value. And only then they react.

All these expectations and reactions can be implemented in many ways. It becomes difficult for us to talk about them, to connect and create them in any high-level form. But we can do better.

That is why we decided to make the part of the magic behind GitHub for Mac: ReactiveCocoa (RAC) publicly accessible. RAC is a framework for composing and transforming value sequences .

')

What is it really?


Let's specify. ReactiveCocoa provides many interesting features:
  1. Ability to compose operations on future data.
  2. The method of decreasing the number of states and mutability.
  3. A declarative way to define behaviors and relationships between properties.
  4. Unified, high-level interface for asynchronous operations.
  5. Excellent API based on KVO.

All of this may seem a bit chaotic until you are aware that the RAC is designed for situations where you are expecting some new value, and then reacting to its change.

The real beauty of RAC is that it can adapt to different, frequently encountered situations.

Enough talk. Let's see how it actually looks like.

Examples


RAC, using KVO, can provide a sequence of values ​​from KVO-compatible properties. For example, we can observe changes in the username property:
[RACObserve(self, username) subscribeNext:^(NSString *newName) { NSLog(@"%@", newName); }]; 

It's cool, but it's nothing more than a good API over KVO. Really interesting things happen when we combine sequences to express complex behavior.

Suppose we want to check whether the user entered a unique name, but only from the first three attempts:
 [[[[RACObserve(self, username) distinctUntilChanged] take:3] filter:^(NSString *newUsername) { return [newUsername isEqualToString:@"joshaber"]; }] subscribeNext:^(id _) { NSLog(@"Hi me!"); }]; 

We watch the username change, weed out insignificant changes, take only the first three values, and then, if the new value is joshaber , we output a special greeting.

So what?

Think about what we would have to do to do this without RAC. Namely:

RAC allows us to do the same with fewer states, fewer template code, better code localization, and a more explicit expression of ours.

What else?

We can combine sequences:
 [[RACSignal combineLatest:@[RACObserve(self, password), RACObserve(self, passwordConfirmation)] reduce:^id(NSString *currentPassword, NSString *currentConfirmPassword) { return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]]; }] subscribeNext:^(NSNumber *passwordsMatch) { self.createEnabled = [passwordsMatch boolValue]; }]; 

Every time the password or passwordConfirmation properties change, we merge their last two values ​​and bring them to BOOL to see if they match. Then we activate or deactivate the create button using the result.

Connections

We can use RAC to get powerful connections with conditions and transformations:
 RAC(self, helpLabel.text) = [[RACObserve(self, help) filter:^(NSString *newHelp) { return newHelp != nil; }] map:^(NSString *newHelp) { return [newHelp uppercaseString]; }]; 

This binds the text of our help label to the help property, if the help property is not nil, and then translates the string to upper case (because users like it when OUT).

Asynchrony

RAC is also quite consistent with asynchronous operations.

For example, we can call a block as soon as several parallel operations are performed:
 [[RACSignal merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] subscribeCompleted:^{ NSLog(@"They're both done!"); }]; 

Or link asynchronous operations:
 [[[[client loginUser] flattenMap:^(id _) { return [client loadCachedMessages]; }] flattenMap:^(id _) { return [client fetchMessages]; }] subscribeCompleted:^{ NSLog(@"Fetched all messages."); }]; 

This authorizes us, receives cached messages, downloads messages from the server and then displays “Fetched all messages.”.

We can also simply transfer the work to the background queue:
 [[[[[client fetchUserWithUsername:@"joshaber"] deliverOn:[RACScheduler scheduler]] map:^(User *user) { // this is on a background queue return [[NSImage alloc] initWithContentsOfURL:user.avatarURL]; }] deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(NSImage *image) { // now we're back on the main queue self.imageView.image = image; }]; 

Or cope with the potential conditions of interception.

For example, we can update a property with the result of any asynchronous call, but only if it was not changed before completion:
 [[[self loadDefaultMessageInBackground] takeUntil:[RACObserve(self.message) skip:1]] toProperty:@keypath(self.message) onObject:self]; 

How it works?


RAC is pretty simple. It consists entirely of signals.

Subscribers are subscribed to signals. Signals send their subscribers next , error , and completed events. And if these are just signals sending events, the key question becomes: when do these events go?

Creating signals

Signals determine their behavior as to when and in connection with which events are sent. We can create our own signal using + [RACSignal createSignal:] :
 RACSignal *helloWorld = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { [subscriber sendNext:@"Hello, "]; [subscriber sendNext:@"world!"]; [subscriber sendCompleted]; return nil; }]; 

The block that we send to + [RACSignal createSignal:] is called each time a signal receives a new subscriber. The new subscriber is substituted into the block and, thus, we can send him events. In the example above, we created a signal that sends “Hello,”, then “world!”, And then ends.

Nested signals

We can also create another signal based on our helloWorld signal:
 RACSignal *joiner = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { NSMutableArray *strings = [NSMutableArray array]; return [helloWorld subscribeNext:^(NSString *x) { [strings addObject:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [subscriber sendNext:[strings componentsJoinedByString:@""]]; [subscriber sendCompleted]; }]; }]; 

Now we have a signal joiner . When someone signs up for a joiner , he automatically signs up for the helloWorld signal. Joiner adds all the values ​​received from helloWorld and then, when helloWorld completes, it combines all the received strings into a single string, sends it and terminates.

In this way, we can create signals based on each other to express complex behavior.

RAC includes a set of operations on RACSignal that are exactly what they do. They take the original signal, returning a new signal with some specific behavior.

Additional Information


ReactiveCocoa works on both Mac and iOS. For more information, you can read the README, as well as the documentation that you can find in the ReactiveCocoa project.

For .NET developers, all this may seem very familiar. ReactiveCocoa is, in fact, the Objective-C version of Reactive Extensions (Rx) for .NET.

Many of the Rx principles apply to RAC. Here are some really good sources about Rx:
Reactive Extensions MSDN entry
Reactive Extensions for .NET Introduction
Rx - Channel 9 videos
Reactive Extensions wiki
101 Rx Samples
Programming Reactive Extensions and LINQ

The translation is published with the consent of the author of the original article and this framework Josh Abernathy.

Source: https://habr.com/ru/post/215033/


All Articles