Transitioning to ReactiveCocoa 4

If you’ve been reading our blog for any period of time, you’ve probably noticed several posts on the topic of Function Reactive Programming and the ReactiveCocoa library. Up until now, all of these posts have been about ReactiveCocoa 2, which was written in and for Objective-C. Now that Swift is gaining in popularity, the ReactiveCocoa developers are working hard on a new version that is written in Swift and leverages some of the functional features that are fundamental to the language.

I suspect that many of you, like myself, have been getting the itch to abandon Objective-C and make the move to Swift. If you are hoping to make use of ReactiveCocoa, I strongly encourage you to give it a try. I’m sure the community would benefit from your contribution. On the other hand, if you are building a professional application for someone, I would caution you to hold off.

The Current State of ReactiveCocoa

At the time of this writing, ReactiveCocoa (referred to as RAC from here out) is in a very unsettled state. When Swift 1.0 was first released, the RAC developers started working on Version 3. However, just recently, Apple released Xcode 7.0, which includes the new Swift 2.0 compiler. This caused some breaking changes to RAC3, but also allowed for some significant simplifications to the API. The change prompted the developers to start working on RAC4.

RAC4 is currently far enough along that it can be used to build an application, but be warned that it is also changing frequently. The change log states, “There may be significant breaking changes in later alphas so be prepared for that before taking a dependency.” That being said, if you’re ready to give it a try, here are some notes to help you get started.

Say Goodbye to RACSignal

In RAC2, all signals used the type RACSignal. A RACSignal could be in either a hot or cold state. A hot signal has at least one subscriber that is listening for events to be sent on it. I think of it like a circuit that is complete and has a path to ground so that current (events) can flow. A cold signal is one that has no subscribers and thus is not facilitating any work or passing any information. It might be completely staged and ready to kick off work as soon as something subscribes, but in the cold state, it’s not doing anything.

RAC4 breaks the concept of signals into two distinct types: SignalProducers and Signals. A SignalProducer is very similar to the old RACSignal; it does nothing until it is started. The developers changed the terminology from subscribed to started because it more accurately describes the state of the signal. For example, a SignalProducer could have multiple observers that are set up to receive events sent on the signal, but the signal won’t become active and the observers won’t see any events until it is started by something. A signal is started by calling start() on it.

The other new type is Signal. A Signal is like a firehose. You hook it up to a source, and events travel through it. There is no starting of a Signal, so you can’t use it to kick off work in the same way a SignalProducer can. Signals also can’t maintain any history. When something begins observing a Signal’s events, it will receive all future events sent on the signal but nothing from the past. Signals are really useful for carrying real-time information that originates from user interactions with the app. For example, a Signal might send the characters that a user enters into a text field.

Defer is Gone

You’ll also notice that RAC4 no longer includes a defer feature. In RAC2, we would often use defer blocks if we wanted to write a function that returned a signal but also performed some other operation (like logging some debug text) when that signal became hot as opposed to when the function is called.

With RAC4, defer is no longer an option. To accomplish the same thing, the authors intend us to observe the started like this.


func logIn(username: String, password: String) -> SignalProducer<String, NoError> {
    return accountManager.logIn(username, password: password)
        .on( started: {
            print("Logging in...")
        }
}

as opposed to,


- (RACSignal *)logInWithUsername:(NSString *)username andPassword:(NSString *)password {
    return [RACSignal defer:^{
        NSLog("Logging in...");
        return [accountManager logInUser:username withPassword:password];
    }];
}

Say Hello to MutableProperties

One feature that I really like in RAC4 is the MutableProperty class. A MutableProperty is a container for an object that can be changed at runtime. It also has a SignalProducer tacked onto it that automatically sends each value that is assigned to observers.

Strict Types

My biggest adjustment in switching to RAC4 has been getting used to the strict types associated with signals. In RAC2, signals knew nothing about the types of objects or errors sent down them. The only restriction was that all objects sent on them must be derived from NSObjects (not primitives).  In RAC4, a signal is defined with a given type for the values sent on it and for the errors that can occur. For example,


var signalA = SignalProducer<String, ReallyBadError> ...

defines a signal that can only send String objects, and only ReallyBadErrors can occur on it. The type safety has some benefits in the sense that it’s much clearer what kind of information is carried on the signal, but it also makes it harder to combine signals.

In RAC2, you could easily take a signal that sends Bananas, merge it with a signal that sends Oranges, and concatenate the result with a signal that sends Habaneros. The resulting signal might not taste good, but it will compile.

In RAC4, the compiler gets very upset if you try to return a signal that sends Apples from a function that is supposed to return a signal that sends Peaches.

Combining Signals

Combining signals is also more difficult because the authors have done away with the nice functions that take arrays of signals to concatenate, merge, etc. For example in Objective-C, if you to concatenate two signals you could do this:


-(RACSignal *) bakeCakeWith:(NSArray *)ingredients {
    RACSignal *preheatSignal = [self.oven preheat:@(350)];
    RACSignal *bakeSignal = [self.baker combine:ingredients];
    return [RACSignal concat:@[preheatSignal, bakeSignal];
}

Now with RAC4, from what I have read, you have to do the following.


func bakeCake(ingredients: Array) -> SignalProducer<RACUnit, NoError> {
    let (combinerSignal, combinerSink) = SignalProducer<RACUnit, NoError>.buffer(1)
    sendNext(combinerSink, self.oven.preheat(350))
    sendNext(combinerSink, self.baker.combine(ingredients))
    return combinerSignal.flatten(.Concat)
}

It’s a bit more verbose, but you could easily make your own generic helper function that makes this just as simple to perform as it is in Objective-C.

I’ve definitely had some moments of frustration getting to know Swift and the new RAC4 framework, but it’s also been a really fun challenge. I plan to keep pressing forward, and I encourage you to do the same.

Conversation
  • “I suspect that many of you, like myself, have been getting the itch to abandon Objective-C and make the move to Swift”

    No! Why?

  • Jordan Schaenzle Jordan says:

    @Michael, Just a few reasons off the top of my head…
    – Much nicer syntax
    – Built-in immutables
    – Tuples as first-class types
    – Functions as first-class types
    – Supports more type safety in the type system
    – Built-in functional constructs like `map`, etc
    – Strings without the crazy @s everywhere
    – Half as many files
    – etc…

  • James says:

    It’s good to see people writing about this. I mostly still live in the RAC2 world for now.

    I thought I should mention that your definition of Hot and Cold aren’t quite how I see them being used.

    A Hot signal generally represents something which pushes events, and subscription is just declaring your interest in those events. No matter how many subscribers (even zero), the same events will be sent.

    A Cold signal represents some work that you might want done, and subscription actually asks for the work to start. This might send a network request, and the subscriber will get the result back at some later time.

    There’s even more confusion in RAC2, because there are signals which are neither exactly Hot or Cold. The RAC4 changes to split into two types are an attempt to address this confusion.

  • Hi Jordan, nice article, thank you!
    One of many reasons I’m still with RAC2 (and objc) is the RAC/RACObserve macro, which makes data binding to UI components really quick when you use the MVVM pattern (ex: RAC(self.myLabel,text) = RACObserve (self, viewModel.title);), how did you manage to accomplish that with RAC4/Swift2 ?

  • Jiaren says:

    Thanks for sharing your experience!

    I used ReactiveCocoa before with Objective-C. It’s ok then.

    I found it’s daunting to integrate RAC4 to Swift, too many changes. Your article helps me to understand those changes! Please keep blogging

  • Comments are closed.