[iOS] [英] ReactiveCocoa vs RxSwift 你选哪个?

Datetime:2016-08-23 01:38:50          Topic:          Share

Functional Reactive Programming is an increasingly popular programming methodology for Swift developers. It can make complex asynchronous code easier to write and understand.

In this article, you’ll compare the two most popular libraries for Functional Reactive Programming: RxSwift vs. ReactiveCocoa.

You’ll start with a brief review of what Functional Reactive Programming is, and then you’ll see a detailed comparison of the two frameworks. By the end, you’ll be able to decide which framework is right for you!

Let’s get reactive!

What Is Functional Reactive Programming?

Note: If you’re already familiar with the concept of Functional Reactive Programming, skip ahead to the next section, titled “ReactiveCocoa vs RxSwift”.

Even before Swift was announced, Functional Reactive Programming (FRP) has seena tremendous rise in popularity in recent years versus Objected Oriented Programming. From Haskell to Go to Javascript, you will find FRP-inspired implementation. Why is this? What’s so special about FRP? Perhaps most importantly, how can you make use of this paradigm in Swift?

Functional Reactive Programming is a programming paradigm that was created by Conal Elliott . His definition has very specific semantics and you are welcome to explore them here . For a more loose/simple definition, Functional Reactive Programming is a combination of two other concepts:

  1. Reactive Programming , which focuses on asynchronous data streams, which you can listen to and react accordingly. To learn more, check out this great introduction .
  2. Functional Programming , which emphasizes calculations via mathematical-style functions, immutability and expressiveness, and minimizes the use of variables and state. To learn more, check out our Swift functional programming tutorial .

Note: André Staltz explores the difference between the original FRP formulation and the practical approach in his article “Why I cannot say FRP but I just did” .

A Simple Example

The easiest way to understand this is through an example. Consider an app that wants to track the user’s location and alert her when she is near a coffee shop.

If you were to program this in an FRP way:

  1. You would construct an object that emits a stream of location events that you can react to.
  2. You would then filter the emitted location events to see which ones are near a coffee shop, and send alerts for those that match.

Here’s what this code might look like in ReactiveCocoa:

locationProducer // 1
  .filter(ifLocationNearCoffeeShops) // 2
  .startWithNext {[weak self] location in // 3
    self?.alertUser(location)
}

Let’s review this section by section:

  1. locationProducer emits an event each time the location changes. Note that in ReactiveCocoa this is called a “signal”, and in RxSwift it’s called a “sequence”.
  2. You then use functional programming techniques to respond to the location updates. The filter method performs exactly the same function as it would on an array, passing each value to the function ifLocationNearCoffeeShops . If the function returns true , the event is allowed to proceed to the next step.
  3. Finally, startWithNext forms a subscription to this (filtered) signal, with the code in the closure expression executed each time an event arrives.

The code above looks very similar to the code you might use for transforming an array of values. But here’s the clever bit … this code is executed asynchronously; the filter function and the closure expression are invoked ‘on demand’ as location events occur.

The syntax might feel a bit strange, but hopefully the underlying intent of this code should be clear. That’s the beauty of functional programming, and why it’s such a natural fit with the whole concept of values over time : it’s declarative. It’s shows you what’s happening , instead of the detail of how it’s being done .

Note: If you want to learn more about the ReactiveCocoa syntax, have a look at a couple of examples I created on GitHub .

Transforming Events

In the location example, you only started to observe the stream, without really doing much with the events aside from filtering the locations for those near coffee shops.

Another fundamental piece in the FRP paradigm is the ability to combine and transform these events into something meaningful. For that, you make use of (but are not limited to) higher order functions.

As expected, you will find the usual suspects you learned about in our Swift functional programming tutorial : map , filter , reduce , combine , and zip .

Let’s modify the location example to skip repeated locations and transform the incoming location (which is a CLLocation ) into a user-friendly message.

locationProducer
  .skipRepeats() // 1
  .filter(ifLocationNearCoffeeShops) 
  .map(toHumanReadableLocation) // 2
  .startWithNext {[weak self] readableLocation in
    self?.alertUser(readableLocation)
}

Let’s look at the two new lines added here:

  1. The first step applies the skipRepeats operations to the events emitted by the locationProducer signal. This is an operation that doesn’t have an array analogue; it is ReactiveCocoa specific. The function it performs is pretty obvious: repeated events (based on equality) are filtered out.
  2. After the filter function is performed, map is used to transform the event data from one type to another, perhaps from a CLLocation to a String .

By now, you should be starting to see some of the FRP’s benefits:

  • It’s simple, yet powerful.
  • Its declarative approach makes code more understandable.
  • Complex flows become easier to manage and represent.

ReactiveCocoa vs RxSwift

Now that you have a better understanding of what FRP is and how it can help make your complex asynchronous flows easier to manage, let’s look at the two most popular FRP frameworks – ReactiveCocoa and RxSwift – and why you might choose one over the other.

Before diving into the details, let’s take a brief look at the history of each framework.

ReactiveCocoa

The ReactiveCocoa framework started life at GitHub. While working on the GitHub Mac client, the developers found themselves struggling with managing their application data-flows. They found inspiration in Microsoft’s ReactiveExtensions , an FRP framework for C#, and created their own Objective-C implementation.

Swift was announced while the team was working on their v2.0 release. They realised that Swift’s functional nature was highly complementary to ReactiveCocoa, so they started work immediately on a Swift implementation, which became v3.0 . The version 3.0 syntax is deeply functional, making use of currying and pipe-forward .

Swift 2.0 introduced protocol-oriented programming , which resulted in another significant ReactiveCocoa API change, with the version 4.0 release dropping the pipe-forward operator in favour of protocol extensions.

ReactiveCocoa is a tremendously popular library with more than 13,000 stars on GitHub at the time of writing.

RxSwift

Microsoft’s ReactiveExtensions inspired many other frameworks that brought FRP concepts to JavaScript, Java, Scala and many other languages. This eventually lead to the formation of ReactiveX , a group which created a common API for FRP implementations; this allowed the various framework authors to work together. As a result, a developer familiar with Scala’s RxScala should find it relatively easy to transition to the Java equivalent, RxJava.

RxSwift is a relatively recent addition to ReactiveX, and thus currently lacks the popularity of ReactiveCocoa (about 4,000 stars on GitHub at the time of writing). However, the fact that RxSwift is part of ReactiveX will no doubt contribute to its popularity and longevity.

It’s interesting to note that both RxSwift and ReactiveCocoa share a common ancestor in ReactiveExtensions!

RxSwift vs. ReactiveCocoa

It’s time to dig into the details. RxSwift and ReactiveCocoa handle several aspects of FRP differently, so let’s take a look at a few of them.

Hot vs. Cold Signals

Imagine that you need to make a network request, parse the response and show it to the user:

let requestFlow = networkRequest.flatMap(parseResponse)
 
requestFlow.startWithNext {[weak self] result in
  self?.showResult(result)
}

The network request will be initiated when you subscribe to the signal (when you use startWithNext ). These signals are called cold , because, as you might have guessed, they are in a “frozen” state until you actually subscribe to them.

On the other hand are hot signals. When you subscribe to one, it might already have started, so you might be observing the third or fourth event. The canonical example would be tapping on a keyboard. It doesn’t really make sense to “start” the tapping, like it makes for a server request.

Let’s recap:

  • A cold signal is a piece of work you start when you subscribe to it. Each new subscriber starts that work. Subscribing to the requestFlow three times means making three network requests.
  • A hot signal can already be sending events. New subscribers don’t start it. Normally UI interactions are hot signals.

ReactiveCocoa provides types for both hot and cold signals: Signal<T, E> and SignalProducer<T, E> , respectively. RxSwift, however, has a single type called Observable<T> which caters to both.

Does having different types to represent hot and cold signals matter?

Personally, I find that knowing the signal’s semantics is important, because it better describes how it is used in a specific context. When dealing with complex systems, that can make a big difference.

Independently of having different types or not, knowing about hot and cold signals is extremely important. As André Staltz puts it:

“If you ignore this, it will come back and bite you brutally. You have been warned.”

If you assume you are dealing with a hot signal and it turns out to be a cold one, you will be starting side effects for each new subscriber. This can have tremendous effects in your application. A common example, would be three or four entities in your app wanting to observe a network request and for each new subscription a different request would be started.

+1 point for ReactiveCocoa!

Error Handling

Before talking about error handling, let’s briefly recap the nature of the events that are dispatched in RxSwift and ReactiveCocoa. In both frameworks, there are three main events:

  1. Next<T> : This event is sent every time a new value (of type T ) is pushed into the stream of events. In the locator example, the T would be a CLLocation .
  2. Completed : Indicates that the stream of events has ended. After this event, no Next<T> or Error<E> is sent.
  3. Error : Indicates an error. In the server request example, this event would be sent if you had a server error. The E represents a type that conforms with the ErrorType protocol. After this event, no Next or Completed is sent.

You might have noticed in the section about hot and cold signals that ReactiveCocoa’s Signal<T, E> and SignalProducer<T, E> have two parameterized types, while RxSwift’s Observable<T> has one. The second type ( E ) refers to a type that complies with the ErrorType protocol. In RxSwift the type is omitted and instead treated internally as a type that complies with ErrorType protocol.

So what does this all mean?

In practical terms, it means that errors can be emitted in number of different ways with RxSwift:

create { observer in
  observer.onError(NSError.init(domain: "NetworkServer", code: 1, userInfo: nil))
}

The above creates a signal (or, in RxSwift terminology, an observable sequence) and immediately emits an error.

Here’s an alternative:

create { observer in
  observer.onError(MyDomainSpecificError.NetworkServer)
}

Since an Observable only enforces that the error must be a type that complies with ErrorType protocol, you can pretty much send anything you want. But it can get a bit awkward, as in the following case:

enum MyDomanSpecificError: ErrorType {
  case NetworkServer
  case Parser
  case Persistence
}
 
func handleError(error: MyDomanSpecificError) {
  // Show alert with the error
}
 
observable.subscribeError {[weak self] error in
  self?.handleError(error)
 }

This won’t work, because the function handleError is expecting a MyDomainSpecificError instead of an ErrorType . You are forced to do two things:

  1. Try to cast the error into a MyDomanSpecificError .
  2. Handle the case where the error is not cast-able to a MyDomanSpecificError .

The first point is easily fixed with an as? , but the second is harder to address. A potential solution is to introduce an Unknown case:

enum MyDomanSpecificError: ErrorType {
  case NetworkServer
  case Parser
  case Persistence
  case Unknown
}
 
observable.subscribeError {[weak self] error in
  self?.handleError(error as? MyDomanSpecificError ?? .Unknown)
}

In ReactiveCocoa, since you “fix” the type when you create a Signal<T, E> or a SignalProducer<T, E> , the compiler will complain if you try to send something else. Bottom line: in ReactiveCocoa, the compiler won’t allow you to send a different error than the one you are expecting.

Another point for ReactiveCocoa!

UI Bindings

The standard iOS APIs, such as UIKit, do not speak in an FRP language. In order to use either RxSwift or ReactiveCocoa you have to bridge these APIs, for example converting taps (which are encoded using target-action) into signals or observables.

As you can imagine, this is a lot of effort, so both ReactiveCocoa and RxSwift provide a number of bridges and bindings out of the box.

ReactiveCocoa brings a lot of baggage from its Objective-C days. You can find a a lot of work already done , which has been bridged to work with Swift. These include UI Binds, and other operators that have not been translated to Swift. This is, of course, slightly weird; you are dealing with types that are not part of the Swift API (like RACSignal ), which forces the user to convert Objective-C types to Swift ones (e.g. with the use of toSignalProducer() method).

Not only that, but I feel I’ve spent more time looking at the source code than the docs, which have been slowly falling behind the times. It’s important to notice, though, that the documentation from a theoretical/mindset point of view is outstanding, but not so much from a usage point of view.

To compensate for this, you can find dozens of ReactiveCocoa tutorials.

On the other hand, RxSwift bindings are a joy to use! Not only do you have a vast catalogue , but there are also a ton of examples , along with more complete documentation . For some people, this is enough reason to pick RxSwift over ReactiveCocoa.

+1 point for RxSwift!

Community

ReactiveCocoa has been around far longer than RxSwift. There are numerous people who could carry on with the work, a fair amount of tutorials about it online, and the Reactive Cocoa tag at StackOverflow is a good source for help.

ReactiveCocoa has a Slack group, but it’s small at only 209 people, so a lot of questions (by myself and others) go unanswered. In times of urgency, I am forced to PM ReactiveCocoa’s core members, and I assume others are doing the same. Still, you can most probably find a tutorial online to explain your particular problem.

RxSwift is newer, and at this time is is pretty much a one man show . It also has a Slack group, and it’s much larger at 961 members, and has more discussion volume. You can also always find someone to help you out with questions there.

Overall, right now both communities are great in different ways so in this category they are about even.

What Should You Pick?

As Ash Furrow said in “ReactiveCocoa vs RxSwift” :

“Listen, if you’re a beginner, it really doesn’t matter. Yes, of course there are technical differences, but they aren’t meaningful to newcomers. Try one framework, then try the other. See for yourself which one you prefer! Then you can figure out why you prefer it.”

I would advise doing the same. Only when you have enough experience will you appreciate the subtleties between them.

However, if you are in a position where you need to pick one and don’t have time to play with both, here’s my suggestion:

Pick ReactiveCocoa if:

  • You want to be able to better describe your system. Having different types to differentiate between hot and cold signals, along with a parameterized type for the error case, will do wonders for your system.
  • Want a battle tested framework, used by many people, in many projects.

Pick RxSwift if:

  • UI Binds are important for your project.
  • You are new to FRP and might need some hand-holding.
  • You already know RxJS or RxJava. Since they and RxSwift are all under the ReactiveX organization, once you know one, the others are just a matter of syntax.

Where to Go From Here?

Whether you choose RxSwift or ReactiveCocoa, you won’t regret it. Both are very capable frameworks that will help you to better describe your system.

It’s also important to mention that once you know RxSwift or ReactiveCocoa, jumping between one and the other will be a matter of hours. From my experience going ReactiveCocoa to RxSwift, as an exercise, the most troubling part was the error handling. Above all, the biggest mind shift is getting into FRP and not a particular implementation.

The following links should help you in your journey into Functional Reactive Programming, RxSwift and ReactiveCocoa:

I hope to see you use one of these great libraries in your future projects. If you have any comments or questions, please join the forum discussion below!