Article summary
The more I work with ReactiveCocoa, the more I enjoy it. But it has a steep learning curve, especially for those that have not had much exposure to Functional Reactive Programming. With each new problem I tackle, I find that I end up iterating from a naive and complicated usage of the library into something quite concise and elegant.
My most recent experience along these lines was when I discovered (and finally understood) -switchToLatest
. Here I will present an example problem, how I implemented a solution at first, and how I was able to refactor that as an FRP light turned on in my head.
The Problem
A TimeKeeper
object has a clock
property that is set to an instance of the Clock
class. The Clock
has a currentTime
property that’s a RACSignal
that signals the current time whenever it changes.
@interface TimeKeeper : NSObject
@property Clock *clock;
@end
@interface Clock : NSObject
@property RACSignal *currentTimeSignal;
@end
Occasionally time needs to speed up or slow down, so the TimeKeeper
can have its clock
property updated with a different implementation of a Clock
(this might be useful in a simulator, for example). Since the clock
property can change, the TimeKeeper
needs to be sure it is listening to its current (or latest) clock
.
The Naive Solution
As I started to work on a solution to this problem, there were a couple of things I wanted to keep in mind:
- Be sure to only react to changes from the current clock.
- Avoid keeping references to previous clocks around.
Here is what I came up with at first:
@interface TimeKeeper ()
@property RACDisposable *previousSubscription;
@end
@implementation TimeKeeper
- (instancetype)init {
if (self = [super init]) {
[RACObserve(self, clock) subscribeNext:^(Clock *clock) {
if (self.previousSubscription) {
[self.previousSubscription dispose];
}
self.previousSubscription = [clock.currentTimeSignal subscribeNext:^(NSDate *time) {
NSLog(@"The current time: %@", time);
}];
}];
}
return self;
}
@end
The first thing to note is that in order to “unsubscribe” from the previous clock, I am holding on to the RACDisposable
returned from subscribeNext:
. The second is that in order to get this to work, I ended up with a subscribeNext:
nested inside of another subscribeNext:
. Both of these things seemed wrong to me, so I went looking for a better way.
The Better switchToLatest Solution
The switchToLatest
method’s documentation states that “The receiver must be a signal of signals” and that it “returns a signal which passes through `next`s and `error`s from the latest signal sent by the receiver, and sends completed` when both the receiver and the last sent signal complete.”
The Basic Operators switching page has a good example.
The docs say that switchToLatest
works on a signal of signals. But I wasn’t dealing with a signal of signals — I was dealing with an object (Clock
) that had a signal (currentTime
), and the Clock
could change from time to time. Once I started to think in terms of state changes being a stream, I realized that I could in fact be dealing with a signal of signals.
I was already using RACObserve
to turn the changes to the clock
property into a signal that I could subscribe to. If instead I just mapped the latest Clock
into its currentTime
signal, I would then have a signal of signals. And using switchToLatest
would ensure that I was only getting updates from the most recent clock
.
Here’s the updated code:
@implementation TimeKeeper
- (instancetype)init {
if (self = [super init]) {
[[[RACObserve(self, clock)
map:^(Clock *clock) {
// Map the changes to the Clock into the currentTime signal
return clock.currentTime;
}]
switchToLatest] // Only interested in signals from the latest Clock
subscribeNext:^(NSDate *time) {
NSLog(@"The current time: %@", time);
];
}
return self;
}
@end
Now there isn’t a need for the previousSubscription
property (goodbye state), and the nested subscribeNext:
calls are gone. Much better.
Conclusion
Until you start to think of everything in terms of signals and streams, ReactiveCocoa (or any FRP library) can be a challenge to work with. But once you make that leap, operators like -switchToLatest
will make sense, and you will see ways to greatly simplify your code.
the TimeKeeper can have its clock property updated with a different implementation of a Clock??
How to do this? could you give some demo code?
Sure. I was thinking of something like this:
Clock *fastClock = [[Clock alloc] initWithSecondsInterval:0.1];
timeKeeper.clock = fastClock;
You could also instantiate a subclass of Clock and assign it. Or even make a chance to the TimeKeeper class so that it’s clock property is Protocol instead of a specific class, and then assign some class that implements that Protocol.
Does that help?
Thank you very much for this article, Patrick. It helps me a lot!
I just created an example project for playing around with switchToLatest and put it on github. If anyone is interested, you’ll find the repo here: https://github.com/itinance/switchToLatestExample
Very cool. Thanks Hagen!
Why not simply using
[RACObserve(self, clock.currentTime) subscribeNext:]
?It will do exactly the same thing in a single line and in a more understandable way.
Philippe,
Thanks for the comment. Unfortunately, I don’t think what you are suggesting is in fact the same thing. I’ll try to explain.
In the example, the Clock class has a currentTime property which is a RACSignal that sends
NSDate values. The line you suggest will watch the currentTime signal of the self.clock object that was present when the RACObserve macro was called. This will cause problems in two respects:
1) The RACObserve is watching a property that is a RACSignal, which means the subscribeNext will receive a RACSignal object instead of the NSDate that was likely desired.
2) The intent of the example was to show how one could swap out the Clock object being used at runtime. The RACObserve you suggest would not notice the new clock when it’s set and would continue watching the original clock’s currentTime property for the duration of the signal.
Does that make sense? Am I missing something? Is there more functionality baked into the RACObserve macro than I’m aware of?
I’d suggest this way which could achieve the same goal I thing.
[[RACObserve(self, clock.currentTime)
switchToLatest] // Only interested in signals from the latest Clock
subscribeNext:^(NSDate *time) {
NSLog(@”The current time: %@”, time);
];