ReactiveCocoa
Bài đăng này đã không được cập nhật trong 8 năm
Introduction
ReactiveCocoa is a functional reactive programming (FRP) framework developed by GitHub. FRP, is a specific way of writing and architecting software that creates a malleable abstraction for timelines; RAC implements one version of it for iOS and OS X. ReactiveCocoa combines a couple of programming styles: Functional Programming which makes use of higher order functions, i.e. functions which take other functions as their arguments Reactive Programming which focuses of data-flows and change propagation. For this reason, we might say ReactiveCocoa described as a Functional Reactive Programming (or FRP) framework.
At the base of FRP is the notion of events. Events are simply things that happen — which is obviously a concept that every type of programming supports. However, in ReactiveCocoa, events are first-class citizens; in fact, they have their own type
enum Event<T, E: ErrorType> {
case Next(T)
case Error(E)
case Completed
case Interrupted
}
The .Error case is the simplest: it represents an error event. The .Next, .Completed and .Interrupted cases are a little different: they imply an ordering.
1. Bindings
The bindings by themselves are just an add-on to the existing KVO mechanism in the Objective-C. A more user-friendly interface, adding to that an ability to describe the rules of binding the state of the model to the state of the UI in the declarative style. Let’s look at the bindings on the example of a cell. Usually a cell links to the model and displays its visual state (or the state of the view model for the MVVM adepts). Although, Reactive Cocoa is often considered in one context with MMVM and vice versa, it doesn’t really matter.
- (void)awakeFromNib {
[super awakeFromNib];
RAC(self, titleLabel.text) = RACObserve(self, model.title);
}
This is the declarative style. "I want the text on my label to always equal the title of my model" -- read in the -awakeFromNib method. It doesn’t really matter when the title or the model changes. When we looked at how it works under the hood, we discovered that RACObserve is a macro that takes keypath (“mode.title” from object self in our case) and converts it to a RACSignal. The RACSignal is an object from Reactive Cocoa framework that represents and delivers the future data. In our example, it will deliver the data from “model.title” every time the model or the title changes.
Quite often you’ll have to transform the state of the model for displaying it in interface. In this case we can use an operator -map:
RAC(self, titleLable.text) = [RACObserve(self, model.title) map:^id(NSString *text) {
return [NSString stringWithFormat:@”title: %@”, text];
}]
All the UI operations must be performed on the main thread. But, for example, the field title can be changed on the background thread (i.e. for data parsing). Here's what we need to add to ensure that a new value of the title will be delivered to the subscriber on the main thread:
RAC(self, titleLabel.text) = [RACObserve(self, model.title) deliverOnMainThread];
RACObserve is a macro expanded to -rac_valuesForKeyPath:observer: But here is a trick -- this macro always captures self as an observer. If we use RACObserve inside a block, we should make sure we’re not causing a retain cycle and use a weak reference if needed. Reactive Cocoa has convenience @weakify and @strongify macros for this needs. One more thing to notice about bindings is a case when your model state is binded to some significant UI changes, and model state changes frequently. This may negatively affect the app performance and to avoid this we can use operator -throttle: - it takes an NSTimeInterval and only sends nexts to the subscriber after the given time interval.
2. Operations on collections (filter, map, reduce)
Working with arrays takes a lot of time. While application is running, the arrays of data are coming from a network (input) and require modifications, so that we can present it to a user in the required form (output). The raw network data needs to be transformed into Models and View Models, and filtered for a user. In Reactive Cocoa, collections are represented as a RACSequence class. There are categories for all types of collections in Cocoa that transform a Cocoa collection to a Reactive Cocoa collection. After these transformations, you’ll get a few functional methods like map, filter, and reduce. Here is a short example from our project:
RACSequence *sequence = [[[matchesViewModels rac_sequence] filter:^BOOL(MatchViewModel *match) {
return [match hasMessages];
}] map:^id(MatchViewModel *match) {
return match.chatViewModel;
}];
Firstly, we filter our view models to select those of them which already have messages (- (BOOL)hasMessages). Then we should transform them into other view models. After done with sequence, it could be transformed back to the NSArray:
NSArray *chatsViewModels = [sequence array];
The great thing in the RAC architecture is that it has just two main classes - RACSignal and - RACSequence, which have a single parent - RACStream. Everything is a stream, but a signal is a push driven stream (new values are pushed to the subscribers and can’t be pulled), whereas a sequence is a pull driven stream (provides values whenever someone asks for them). One more thing to notice is how we chained the operations together. It’s the key concept in the RAC, which is also applied to both the RACSignal and the RACSequence.
3. Networking
The next step in understanding the features of the framework is using it for networking. When I talked about bindings, I mentioned that RACObserve marco created a RACSignal which represents the data that will be delivered in the future. This object is perfect for representing a network request. Signals send three types of events:
- next - the future value/values;
- error - an NSError* value that indicates that the signal can’t be completed successfully;
- completed - indicates that the signal has been completed successfully.
The lifetime of a signal consists of any number of next events, followed by one error or completed event (but not both). This is quite similar to how we used to write our network requests using blocks. But what’s the difference? Why to replace usual blocks with signals? Here are some reasons:
- You get rid of the callback hell! This nightmare in code happens when you have a few nested requests and each subsequent one uses the result of the previous one.
- You propagate error in one place. Here is a short example: Suppose you have two signals -- loginUser and fetchUserInfo. Let’s create a signal that logs in a user and then fetches his info:
RACSignal *signal = [[networkClient loginUser] flattenMap:^RACStream *(User *user) {
return [networkClient fetchUserInfo:user];
}];
flattenMap block will be called when loginUser signal sends the next value and this value gets passed to the block via the user parameter". In flattenMap block we take this value from the previous signal and produce a new signal as a result. Now, let’s subscribe to this signal:
[signal subscribeError:^(NSError *error) {
// error from one of the signals
} completed:^{
// side effects goes here. block get called when both signals completed
}];
It’s worth mentioning that subscribeError block will be called if at least one of the signals fails. If the first signal completes with an error, the second one will not be executed.
Conclusion
The functional reactive approach can simplify the way you make solutions in your daily tasks. Perhaps, at first, the concept of RAC may seem too complicated, its solutions too cumbersome, and bulky, and the number of operators will confuse you a lot. But later, it’ll become clear that all of this has a simple idea behind.
Source
All rights reserved