In the last couple posts, we’ve discussed how we landed on reactive programming,
as well as the seven layer dip that is RxSwift. Thus far, we haven’t really
spoken much about Combine
, Apple’s shiny new framework that seems to ape
be inspired by RxSwift.
In order to discuss a Combine, one has to discuss the main differences between it and RxSwift. To my eyes: there are three.
- Affordances for non-reactive classes
- Error handling
- Backpressure
I’ll be splitting each of those into their own posts over the next week or so. Let’s start with the first one.
RxCocoa Affordances
In my prior post, we discussed that RxSwift is more than just… RxSwift. It actually includes many, many affordances for UIKit controls in the sorta-but-not-really sub-project RxCocoa. Additionally, RxSwiftCommunity steps up and provides a lot of bindings for the more remote outposts of UIKit, as well as other CocoaTouch classes that RxSwift and RxCocoa don’t cover.
This makes it impossibly easy to get an Observable
stream from, say,
a UIButton
being tapped. From my post:
let disposeBag = DisposeBag()
let button = UIButton()
button.rx.tap
.subscribe(onNext: { _ in
print("Tap!")
})
.disposed(by: disposeBag)
Easy peasy.
Let’s [Finally] Talk About Combine
Combine is very much like RxSwift. Pulling from the documentation, Combine self-describes as such:
The Combine framework provides a declarative Swift API for processing values over time
This should sound familiar; look at how ReactiveX (the parent project of RxSwift) describes itself:
An API for asynchronous programming with observable streams
These are actually saying the same thing; the ReactiveX version is simply using some domain language. It could be rephrased as:
An API for asynchronous programming with values over time
That’s pretty much the same thing in my book.
Same As it Ever Was
As I started looking into the API, it was quickly obvious that most of the types I’m familiar with from RxSwift have approximations in Combine:
Observable
→Publisher
Observer
→Subscriber
Disposable
→Cancellable
This is a huge marketing win; I cannot tell you the amount of “🙄” I got from otherwise open-minded developers as soon as I started describing RxSwift’sDisposable
.SchedulerType
→Scheduler
So far so good. I can’t help but reiterate how much I prefer “Cancellable” over “Disposable”. That’s an incredibly great change not only from a marketing perspective, but also because it more accurately describes what that object is.
But things continue to get better!
- RxCocoa’s
Driver
→ SwiftUI’sBindableObject
This is a little bit of a reach, but they spiritually serve the same purpose, and neither of them can error. Single
→Future
SubjectType
→Subject
PublishSubject
→PassthroughSubject
So far, we’re off to the races.
Let’s Take a Hot Chocolate Break
Everything takes a turn once you start diving into RxCocoa. Remember our
example above, where we wanted to get an Observable
stream that represents
taps of a UIButton
? Here it is again:
let disposeBag = DisposeBag()
let button = UIButton()
button.rx.tap
.subscribe(onNext: { _ in
print("Tap!")
})
.disposed(by: disposeBag)
To do the same in Combine requires… a lot more work.
🚨🚨🚨🚨🚨🚨🚨
Combine does not include any affordances
for binding to UIKit objects.
🚨🚨🚨🚨🚨🚨🚨
This… is a serious fucking bummer.
Here’s a generic way to get a UIControl.Event
out of a
UIControl
using Combine:
class ControlPublisher<T: UIControl>: Publisher {
typealias ControlEvent = (control: UIControl, event: UIControl.Event)
typealias Output = ControlEvent
typealias Failure = Never
let subject = PassthroughSubject<Output, Failure>()
convenience init(control: UIControl, event: UIControl.Event) {
self.init(control: control, events: [event])
}
init(control: UIControl, events: [UIControl.Event]) {
for event in events {
control.addTarget(self, action: #selector(controlAction), for: event)
}
}
@objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) {
subject.send(ControlEvent(control: sender, event: event))
}
func receive<S>(subscriber: S) where S :
Subscriber,
ControlPublisher.Failure == S.Failure,
ControlPublisher.Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
The above is… considerably more work. On the plus side, however, the call site is reasonably similar:
ControlPublisher(control: self.button, event: .touchUpInside)
.sink { print("Tap!") }
By comparison, RxCocoa brings us that sweet, delicious, hot chocolate, in the form of bindings to UIKit objects:
self.button.rx.tap
.subscribe(onNext: { _ in
print("Tap!")
})
In and of itself, these call sites are, quite similar indeed. It’s all
the work I had to do writing ControlPublisher
myself to get to this point
that’s the real bummer. Furthermore, RxSwift and RxCocoa are very well
tested and have been deployed in projects far bigger than mine.
By comparison, my bespoke ControlPublisher
hasn’t seen the light of day
until… now. Just by virtue of the amount of clients (zero) and time in the
real world (effectively zero compared to RxCocoa), my code is infinitely more
dangerous.
Bummer.
Enter the Community?
To be fair, there is nothing stopping the community from putting together a sort of open source “CombineCocoa” that fills the gap of RxCocoa in the same way that RxSwiftCommunity works.
Nevertheless, I find this to be an exceptionally large ❌ on Combine’s scorecard. I’m not looking to rewrite all of RxCocoa simply to get bindings to UIKit objects.
If I’m willing to go all-in on SwiftUI, I suppose that would take the sting off of these missing bindings. Even my young app has a ton of UI code in it. To throw that out simply to jump on the Combine bandwagon seems foolish at best, and dangerous at worst.
More to Come
In my next post, we’ll discuss error handling in RxSwift and Combine. Some different design decisions were made between the two projects, and I could make a passionate argument that both are correct. Stay tuned.