ReactiveCocoa: Understanding switchToLatest

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.
 

Conversation
  • Hu says:

    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?

  • Patrick Bacon Patrick Bacon says:

    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?

  • Hagen says:

    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

  • Patrick Bacon Patrick Bacon says:

    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.

  • Patrick Bacon Patrick Bacon says:

    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?

  • Jackie says:

    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);
    ];

  • Comments are closed.