Climbing the Reactive
Learning Curve
in Swift
TL;DR
There’s a pretty cool programming trend that strives to model all data flow as transformable streams. Variations of this model go by FRP, Reactive Programming, Rx, and RAC. I tested 4 Swift libraries that tackle this idiom, and, in the end, chose RxSwift as the one I’d use in production. The primary influence in that decision was documentation as my biggest stumbling block is simply getting my brain to stop thinking so procedurally all the time!
My learning path, should you choose to follow it, went something like this:
- Read The introduction to Reactive Programming you’ve been missing
- Browse around the ReactiveX site.
- Play with Rx Marbles
- Spend a full day working through this JavaScript Rx tutorial
TS;WR
I recently posted to a Swift Programming Language forum in hopes of getting advice for how to choose between the various open source frameworks available for Functional Reactive Programming (FRP*) in Swift. Since no one replied, I decided try all four in roughly identical sample projects.
*With apologies to Conal Elliott.
If you want to follow along, download the sample code from github and check out the relevant commit for each section. Caveat lector: I drafted this writeup just before a long vacation, so it’s all Swift 1.2 | Xcode 6.
Each of these frameworks has a Swift 2 branch and is liable to change significantly with the release of Swift 2 | Xcode 7
Oh, and here are some quick links to the projects in question:
Interstellar: https://github.com/JensRavens/Interstellar
ReactiveCocoa: https://github.com/ReactiveCocoa/ReactiveCocoa
RxSwift: https://github.com/ReactiveX/RxSwift
ReactKit: https://github.com/ReactKit/ReactKit
Commit #1 — Create Xcode Projects
- File » New Project » Single View » Swift (x4) » no git
- Add all 4 projects to a single git repo
Commit #2 — Add Framework (x4)
- Install CocoaPods. Note: ReactiveCocoa (RAC) has official support only for Carthage, but unofficial pod support seems to do the trick.
- `pod init`
- Update PodFile
- `pod install`
Commit #3 — Some basic UI
Starting simple: a text field + a label. No functionality yet, just hooked up the views’ outlets; I want the reactive coding to be clearly visible in the commit diffs!
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel! override func viewDidLoad() {
super.viewDidLoad()
}
…
Commit #4 — First Signal Implementations
Interstellar
import UIKit
import Interstellarvar TypingSignalHandle: UInt8 = 0extension UITextField: UITextFieldDelegate {
public var typingSignal: Signal<String> {
let signal: Signal<String>
<snip> // dip into Objective-C to modify UITextField so that
// a text field can become its own delegate
return signal
}
<snip> // transform textField:shouldChangeCharactersInRange:…
// into a signal, aka stream, aka observable
…
OK! Now we’re ready to run in the big leagues:
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel! override func viewDidLoad() {
super.viewDidLoad() textField.typingSignal.next {
string in
self.label.text = string
}
}
…
No built-in UIKit signals. Followed the UISearchBar extension example from ReactiveKitten to get a simple text signal going.
ReactiveCocoa
import UIKit
import ReactiveCocoaclass ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel! override func viewDidLoad() {
super.viewDidLoad() textField.rac_textSignal().subscribeNextAs {
(string: String) in
self.label.text = string
}
}
…
Built-in rac_textSignal() is implemented in Objective-C, so strong typing requires an extension to avoid having to unwrap optionals every time. Thanks for the tip, http://blog.scottlogic.com/2014/07/24/mvvm-reactivecocoa-swift.html !
RxSwift + RxCocoa
import UIKit
import RxSwift
import RxCocoaclass ViewController: UIViewController { @IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel! override func viewDidLoad() {
super.viewDidLoad() textField.rx_text >- subscribeNext {
string in
self.label.text = string
}
}
…
Worked simply & out of the box. I like how the cocoa extensions are in a separate project!
ReactKit
Worked out of the box, but with issues:
- Required declaration of a stream variable whose sole purpose seems to be type specification.
- The right reacting operator (~>) wasn’t interchangeable with its mirror-image (<~).
import UIKit
import ReactKitclass ViewController: UIViewController { @IBOutlet weak var textField: UITextField!
@IBOutlet weak var label: UILabel! var stream: Stream<NSString?>? override func viewDidLoad() {
super.viewDidLoad() stream = textField?.textChangedStream()
(label, “text”) <~ stream!
// Why doesn’t the following work?!
// stream! ~> (label, “text”)
}
…
- Relying on strings + KVC seems fragile and not very Swifty.
Round 1 Score
Interstellar: 0 | ReactiveCocoa: 0 | RxSwift: 1 | ReactKit: 1 (w/reservations)
Commit #5 — Throttling
Throttling functions “swallow” values that would otherwise be delivered before the throttle time. Here, we only take values that occur after the
Interstellar doesn’t have this pretty standard function, even though it probably wouldn’t add much ‘weight,’ and it should be doable without additional dependencies outside Foundation. (And my own reactive skills aren’t up to the task just yet.) Too bad. Let’s see how the other three compare.
ReactiveCocoa
1-liner: `.throttle(1.0)`
Caveat: It appears that throttle may have been implemented “incorrectly” before version 3.
…
override func viewDidLoad() {
super.viewDidLoad() textField.rac_textSignal()
.throttle(1.0)
.subscribeNextAs {
(string: String) in
self.label.text = string
}
}
…
RxSwift + RxCocoa
Not so simple as a ‘scheduler’ is required in addition to the throttle time interval. RAC seems to have a cleaner model for switching queues, but perhaps it makes sense to only allow thread/queue changes on particular functions. ?
…
override func viewDidLoad() {
super.viewDidLoad() textField.rx_text >-
throttle(1.0, MainScheduler.sharedInstance) >-
subscribeNext {
string in
self.label.text = string
}
}
…
ReactKit
Not too difficult (ran into weird Swift errors due to a misplaced `?` on the textField variable, but that’s partly just about the current state of Swift and Xcode). Somewhat worrisome: the functionality seems slightly borked: some characters appear to get swallowed, even during a pause…until more typing happens, at which point they all show up simultaneously.
…
override func viewDidLoad() {
super.viewDidLoad() stream = textField!.textChangedStream()
|> throttle(1.0)
(label, “text”) <~ stream!
}
…
Round 2 Score
Interstellar: -1| ReactiveCocoa: 1 | RxSwift: 1 | ReactKit: 0
Interstellar: 0 | ReactiveCocoa: 0 | RxSwift: 1 | ReactKit: 0
Commit #6 — Map
This is the most basic of tests. Here, we force the typed value to be an integer, then double it before updating the label.
Interstellar
Worked simply & out of the box.
…
override func viewDidLoad() {
super.viewDidLoad() textField.typingSignal
.map { return $0.toInt() ?? 0 }
.map { return $0 * 2 }
.next {
i in
self.label.text = “\(i)”
}
}
…
ReactiveCocoa
Hampered once again by Objective-C roots: I wasn’t able to inline the doubling code. I guess I could have written another extension for auto-typecasting map…this is getting annoying.
Worked simply & out of the box.
…
func dubble(string: AnyObject!) -> AnyObject! {
if let str = string as? String, let i = str.toInt() {
return i * 2
}
return 0
}override func viewDidLoad() {
super.viewDidLoad() textField.rac_textSignal()
.throttle(1.0)
.map(dubble)
.subscribeNextAs {
(i: Int) in
self.label.text = “\(i)”
}
}
RxSwift + RxCocoa
While ReactiveCocoa had type cast annoyances and ReactKit just didn’t seem to work, both Interstellar and RxSwift still required wrangling Swift types in unexpected ways. For inlining, this worked just fine:
>- map { $0.toInt() ?? 0 }
>- map { $0 * 2 }
…substituting `.` for `>- ` in the case of Interstellar…but I just couldn’t figure out a way to combine the two into a single inline closure. Perhaps Swift simply can’t infer the closure’s return type, despite all of the returns resolving to Int? Hard to say as the error we get from the code below is super vague: “Could not find an overload for ‘map’ that accepts the supplied arguments.”
>- map {
if let i = $0.toInt() { return i * 2 }
return 0
}
ReactKit
Couldn’t get `map` to behave at all as expected in ReactKit. :(
Round 3Score
Interstellar: 0 | ReactiveCocoa: 1 | RxSwift: 2 | ReactKit: 0
Wrapping it up for now
I really liked being able to get up and running in no time with Interstellar, but it seems like I won’t be able to get far with it in the long run without a lot of extra customizations. Since I’m still just dipping my toes in the water, I don’t trust myself to get those customizations right, and their absence is certainly not going to save me any time.
Interstellar eliminated. Darnit.
Having got a ton of insight from attending Yasuhiro Inami’s talk, I kind of hoped ReactKit would come out at-or-near the top. I still recommend the talk since he did a great job of pulling back the curtains on the mysterious wizard that is FRP! Meanwhile, for me at least.
ReactKit eliminated. Shoot.
That leaves ReactiveCocoa and RxSwift.
ReactiveCocoa’s history and massive adoption weigh heavily in its favor. As a framework, it is unlikely to lose support any time soon. On the other hand, that same history is a double-edged sword. As of this writing, RAC is still something like 4/5 Objective-C code. No telling how long before The Great Swiftening is complete, but for now there’s still a fair amount of unnecessary explicit casting from AnyObject required. There’s also a ton of legacy documentation for the old way, which means getting help and examples will be hit-or-miss for awhile.
If RxSwift had only one contributor, or if it had any glaring issues like ReactKit’s broken `throttle`, I’d be very wary. But it currently has a healthy 11 contributors, the mutual support/endorsement of ReactiveX, and offers Cocoa extensions as an a la cart addon rather than monolithically bundling them in as RAC has. While RxSwift’s documentation and 3rd party blog posts/sample code don’t suffer from RAC’s noisy history, it’s also not as fleshed out, sometimes simply deferring completely to the language-agnostic reactivex.io.
I’d say it’s a tie, with you as the tie breaker. For my part: ReactiveCocoa eliminated. I’m going with RxSwift.
Sample code available on github.