By Casey Liss
Combine: Where's the Beef?

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:

  • ObservablePublisher
  • ObserverSubscriber
  • DisposableCancellable
    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’s Disposable.
  • SchedulerTypeScheduler

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’s BindableObject
    This is a little bit of a reach, but they spiritually serve the same purpose, and neither of them can error.
  • SingleFuture
  • SubjectTypeSubject
  • PublishSubjectPassthroughSubject

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.