Late last week I joined my pals Aleen Simms and Stephen Hackett on the Relay FM members-only show, Fusion.
We discussed how Aleen and I are both in the market for new laptops. My beloved MacBook “Adorable” is getting a bit long in the tooth, and I’m really considering replacing it soon. Aleen is in a similar situation. Stepehen attempts to be our guide as we navigate these new waters.
If you’d like to become a Relay FM member, may I politely but strongly suggest selected Analog(ue) as your choice of show to support? 😇
Yesterady I joined Lory Gil, Dan Moren, and Mikah Sargent on Clockwise. On this episode, we discussed Jony Ive’s recent departure from Apple, unplugging on vacation, the stickiness of gamification, and outdoor tech that we enjoy and/or are lusting after.
Clockwise stuffs an incredible amount of content into a very short time; you really can’t go wrong. As always, this was a fun one.
I have a confession to make:
I’m a monster.
I’m a monster who occasionally buys movies on optical disc.
In 2019, that’s probably a stupid thing to do. I’m a man of habit, and like good reflexes, they die hard.
A friend of mine was recently asking me how I ripped optical discs for use in Plex. The process is pretty easy, but has a couple of potential gotchas.
In broad strokes:
- Get the video off the disc and store it as a
MKV
file. - Compress it in such a way that any normal human won’t be able to tell the difference between the original and the compressed file.
MKV
s are nice because they’re generic containers that can hold
pretty much any kind of audio or video content. Since there are
no restrictions about the kinds/codecs of content in a MKV
, it
is a great choice of container; whatever is on the disc can be
stored within a MKV
.
However, as we’ll learn later, for long-term storage, I actually prefer something different.
Setup
- Get yourself a DVD or BluRay player.
There are many, many options for external drives that plug into your computer via USB. I bought a really cheap one that, unsurprisingly, was a piece of garbage. I replaced it with this Pioneer drive which is excellent. - Download MakeMKV.
It’s available for both macOS and Windows. - Pay for it.
It’s a great app, and at $50, it’s as much as a couple of BluRays. - Install it. Note that on macOS, MakeMKV isn’t appropriately
signed, so the first time you run it, you’ll need to find it
in
/Applications
, right-click on it, and selectOpen
. - Plug in your drive, place the disc in it, and run MakeMKV.
Rip: Easy Mode
- When you run MakeMKV, the first step is to scan the disc and see what’s on it. You do so by clicking the huge icon on the left:

- Once you tap the icon, MakeMKV scans what’s on the disc and will present you with a list of titles, and within each title, chapters.
- If things go according to plan, and if you’re ripping a film,
it should be quite easy to figure out which title to rip. It
will be the one which is largest; usually 4GB+ for DVDs and
20GB+ for BluRays.
If you see more than one option, we’ll cover that later.

- Once you discover which title you want to rip:
- You’ll probably want to un-check all the other titles on the disc, as you probably don’t want to rip those.
- For the title you do want, you’ll probably want to un-check any of the subtitles or audio streams you don’t want. I typically find I’m un-checking Spanish and French on most discs.
- Set the path for the
MKV
file that MakeMKV generates, and then click the icon on the right. This will copy the disc to your computer.
Rip: Hard Mode
Depending on the disc, things can get pretty dodgy when it comes to picking which title to rip. Sometimes you can run into scenarios like this one:

Notice that two different titles are both effectively the same size: 34.4 GB.
Sometimes this is a language issue: even though one title can hold multiple languages of audio, or even subtitles, sometimes films will have different editions to change languages of text shown on screen. Things like signs, titles shown on-screen, etc. Animated films, like those made by Pixar, tend to do this quite a lot with on-screen signage.
However, many times, multiple [near-] identically sized titles are a rudimentary form of copy protection. Often times there is one clear winner, and a bunch of fakes that, when played back, aren’t quite right.
So, what’s the solution?
My first approach is to do a search; sometimes you can find a
forum post that answers the question. When you do find a post that
answers the question, it will be answered in the form of a mpls
to rip. An example could be 00800.mpls
.
If your search comes up empty, you could try to play the disc
and see if your playback software/device will tell you what
mpls
it’s playing. In my experience, that’s not something most
players will tell you.
Alternatively, you can take the brute-force approach, which is what I usually do: rip everything and just try playing them. I take a look for on-screen text and forced subtitles. I also scrub through the file and make sure it looks, at a glance, like everything happens in the correct order.
Whichever one is in English, and seems to have all the right stuff in the right place, is the winner.
Compression
At this point, you could choose to stop. The MKV
that
MakeMKV has created is perfectly playable by most software,
such as Plex or IINA. However, these files are
not compressed, and thus they are huge. Our example
above was ~35 GB.
Personally, even though I do have a massive NAS, I’d rather compress these files a bit for storage.
Enter Don Melton’s fantastic video transcoding tools.
You may know Don as the father of the Safari web browser, but in his retirement, he’s kept his hands busy by writing a suite of incredibly good tools to help transcode and compress video.
In my experience, after running a video through Don’s tools, there is virtually no degradation in quality, despite the output file being 10-20% of the size of the original.
Don’s scripts do use the command line, and the installation can be a little bit fiddly. Installation is left as an exercise for the reader, but once you get everything installed, usage couldn’t be simpler:
transcode-video file-that-makemkv-created.mkv
That being said, I do prefer to provide a couple of options. So,
let’s say I had a file avengers.mkv
. I would run that file
through Don’s tools as such:
transcode-video --mp4 --burn-subtitle scan avengers.mkv
The two options:
--mp4
indicates that I want the resulting output to be a
MP4
file, rather than another MKV
. This tends to play
better with Apple devices, which is what I use to play these files.
--burn-subtitle scan
is a little bit more nuanced. In some
films, foreign-language content spoken on-screen will be displayed
in English by forcing the subtitles to show during that time,
even if subtitles haven’t been turned on by the user. A great
example of this is The Hunt for Red October, when Russian is
spoken on-screen.
--burn-subtitle scan
indicates to Don’s tools that they should
attempt to scan for any forced subtitles, and then burn them
into the video, so they’re part of image shown on-screen. As
Don writes in his instructions:
By default, the transcode-video tool automatically burns any forced subtitle track it detects into the output video track. “Burning” means that the subtitle becomes part of the video itself and isn’t retained as a separate track. A “forced” subtitle track is detected by a special flag on that track in the input.
You can also use a special “scan” mode […] to find any embedded forced subtitle track that’s in the same language as the main audio track.
Be aware that using this special “scan” mode does not always work. Sometimes it won’t find any track or, worse, it will find the wrong track. And you won’t know whether it worked until the transcoding is complete.
Personally, I find it infuriating when I try to watch a movie
but the subtitles weren’t properly burnt in. To me, it’s worth
using --burn-subtitle scan
on anything that has some foreign
language content. You may feel differently.
I’ve only recently gotten in the habit of instructing Don’s tools to burn in subtitles, but they haven’t failed me… yet.
Fin
The transcode-video
process will take quite a while. When it’s
done, you’ll end up with a single file that has a compressed
version of the disc you started with. By
naming the file appropriately, you can easily add it to
Plex, where all the relevant metadata will be added automatically.
Now, you can watch your movie immediately, without having to sit through 2 minutes of FBI warnings and a ridiculous menu. Imagine that.
I was watching some WWDC sessions, and stumbled upon an interesting moment in #713: Advances in Networking, Part 2:

This section starts at just shy of 56 minutes, and runs for about two and a half minutes. The presenter is Stuart Cheshire, who you may know as coming up with Bonjour. (❗❗)
One can never really know if Vignette inspired this slide, but in my head canon, I’m going to choose to believe it.
And if you ever stumble on this, thanks, Stuart. You really made my week. 😄
Phew, it’s been a busy month. 😅
Just before WWDC, I joined my pal Tyler Stalman on his show, The Stalman Podcast. On this episode, Tyler and I discussed what it’s like to become an independent developer and build an indie app.
Now that things are finally settling down, I have the time to give the episode the link post it deserves.
Recorded just a few days after Vignette’s release, I was still embroiled in the post-launch insanity. I was also preparing for my then-days-away departure for WWDC. Nevertheless, it was a really great discussion about what it’s like to start from no knowledge of programming, and build yourself up to your own app.
The Stalman Podcast is a wide-ranging show, and that’s what makes it so great. I always consider myself lucky when Tyler asks me to sit in and bring a bit of my kind of nerdiness to the show. :)
Over the last several days, I’ve gone on a deep dive on the differences between RxSwift and Combine. To recap:
- How do we get to
Observables
? - What is RxSwift anyway?
- Where are UIKit bindings in Combine?
- What’s new in Seed 2? Can KVO save us?
- When do we specify how things can go wrong?
- Who is in charge of saying “no”
We’ve been on a long journey, so let’s go ahead and wrap this up… for now.
Overall Impressions
Naturally, there’s a lot to love in Combine. So much of Combine is exactly what I’ve been hoping for: a first-party, blessed, approved, framework for reactive programming on Apple platforms. To my eyes, a tremendous amount of Combine was inspired by the ReactiveX project, of which RxSwift is a part.
A lot of what I’m used to is already there, even if the names have been changed.
In some cases, such as DisposeBag
becoming Cancellable
, I
think the change is a dramatic improvement. In most other cases, I don’t have
a strong opinion one way or the other. Which, ultimately, is a win.
Furthermore, there are some differences in the way RxSwift and Combine are designed. RxSwift doesn’t bother itself with typed errors nor backpressure. That leads for easier bookkeeping, but sometimes far more convoluted code. Combine, by comparison, bakes both typed errors and backpressure in from the start. More bookkeeping, but more robust code.
Overall, Combine today is definitely a “1.0” release. There’s a lot that is still missing from Combine. There is no clear way to bind to UIKit objects; the most obvious answer is KVO, but KVO isn’t available on most UIKit objects. Naturally, one can write these bindings by hand, but that’s fraught with peril, and a lot of work.
Plans for Vignette
All of this makes my planning for Vignette… complicated.
Currently, my plan is to keep on developing Vignette using RxSwift. There’s just too much missing from Combine that I’d have to give up and re-write to use it in lieu of RxSwift.
However.
Vignette’s UI is trickier than you’d expect, but still not that complex, in the grand scheme of things. And the siren call of SwiftUI is very, very strong. To affect state changes in SwiftUI, Combine is preferred.
💩
Having only played with SwiftUI a couple times, my current plan is:
- Any new views will be SwiftUI wherever possible.
- Existing views will be ported as time allows and as seems reasonable.
For existing views that I port, my current plan is to bridge from
RxSwift → Combine. Thankfully, the way I write my apps makes this
reasonably easy, as there is one Observable<State>
that will need to be
converted into a Producer
. Everything is contained in that one stream,
so it’s not like I have to assemble 350 output streams in order to bridge
into the Combine world.
(This could get real ugly on the input side, however, as what goes into my
state generator is a ton of UIKit-sourced Observables
. I’m still not sure
how this would play out in a SwiftUI world.)
Greenfield Apps
For a greenfield app, I’m not sure what I’d do.
I think if I could make 100% of the app SwiftUI, I would do so, and use only Combine. To avoid pulling in the large RxSwift dependency would be awesome. However, the moment I need to do more than one or two things in UIKit, I’d probably have second thoughts.
Existing Apps Without RxSwift
If I was looking at what to do with an app that doesn’t use RxSwift, but does use UIKit, I’d probably avoid retrofitting Combine onto any of it. However, I’d absolutely do all new development in SwiftUI and Combine going forward. It is clearly the future; embrace it.
Where Do We Go From Here?
For now, this series on RxSwift vs. Combine is over. I’ve said all I can say without really diving into how this stuff works. The best way to do that, of course, is to use it.
Over the summer I plan to do some compulsory updates to Vignette, like dark mode support, and then start dabbling with SwiftUI and some of this new Combine hotness. As I do so, I’ll surely be putting up new posts describing my findings.
Need Help?
Should you find yourself in the position of needing some assistance with your own RxSwift (or straight UIKit) app, and want me to come in and take a look, please reach out. I have plenty of my own things to keep me busy this summer, but if the fit is right, I’d love to help out some other users. In no small part to help me learn too. 😊
I’ve been spending the last several days discussing the differences between RxSwift and Apple’s new Combine framework:
- How do we get to
Observables
? - What is RxSwift anyway?
- Where are UIKit bindings in Combine?
- What’s new in Seed 2? Can KVO save us?
- When do we specify how things can go wrong?
Today, we’ll discuss backpressure.
An Illustrated Example
Do you remember this famous scene from I Love Lucy?
If you’re one of the six people on the planet who hasn’t seen it, Lucy and Ethel are attempting to wrap chocolates as they come down a conveyor belt. Before long, the chocolates come far faster than the women can handle, and things get interesting. And hilarious.
This short video is actually a phenomenal example of backpressure.
Backpressure
In the video above, the chocolates coming down the conveyor belt
are basically an Observable
/Producer
. The chocolates were
being emitted at whatever speed they wanted to be, and that was
that.
The conveyor/Observable
/Producer
operating at whatever speed
it sees fit makes for great comedy. However, it can make for some
complicated circumstances in code.
Let’s suppose you’re writing a banking iOS app. Perhaps somewhere in your app you have a mechanism for depositing a check by taking a picture of it. At the end of that process is a button which the user taps to actually commit the deposit.
A nefarious user wants to see if they can get some free money. They decide to mash down on that button a zillion times in a row, hoping your app won’t be smart enough to handle it appropriately. They hope that instead you’ll repeat the deposit a zillion times, and effectively make it rain for them. Suddenly, we have an I Love Lucy scenario: the button taps are coming down the conveyor belt far faster than we can handle them.
(Naturally there a zillion other ways to handle this — most notably immediately disabling the button — but just roll with me on this, m’kay?)
What Lucy needed, and what we need in this contrived example, is a way to say “I’ll take just one pleaseandthankyou”. We need a way to throttle the speed with which chocolates are sent down the conveyor belt, and clicks are sent down that stream.
I’ve Got Your Backpressure Right Here
RxSwift takes an interesting approach to backpressure.
🚨🚨🚨🚨🚨🚨🚨
RxSwift does not include any affordances
for dealing with backpressure.
🚨🚨🚨🚨🚨🚨🚨
In RxSwift, we would have been no better off than Lucy. Those chocolates would have kept coming, whether or not we could handle them. Some of the projects under the ReactiveX umbrella do handle backpressure, but RxSwift is not one of them. For more, you can read the official ReactiveX entry on backpressure. In short, it pretty much says “good luck”.
Combining Flow and Pressure
Likely unsurprisingly by now, Combine takes a different approach to backpressure: it’s built into the system.
Look at the [slightly simplified] definition for protocol Subscriber
:
protocol Subscriber {
associatedtype Input
associatedtype Failure : Error
// Notifies the subscriber that it has successfully subscribed
func receive(subscription: Subscription)
// Notifies the subscriber that there is a new element; the
// equivalent of RxSwift's onNext()
func receive(_ input: Self.Input) -> Subscribers.Demand
// Notifies the subscriber that it has completed; the
// equivalent of both RxSwift's onCompleted() and onError()
func receive(completion:)
}
Wait a second. In RxSwift’s Observer
, things looked a little different:
protocol Observer {
func onNext(Element)
func onComplete()
func onError(Error)
}
Do you see the difference there? I don’t mean the splitting out of
onError()
and onComplete()
. Look at the return types. Specifically,
look at the return types for receive(input:)
and onNext(Element)
:
// Combine
func receive(_ input: Self.Input) -> Subscribers.Demand
// RxSwift
func onNext(Element)
RxSwift’s onNext()
doesn’t return anything, whereas Combine’s
receive(input:)
does. So what the hell is Subscribers.Demand
‽
Simplified, here it is:
public enum Demand {
case unlimited
case max(Int)
}
There’s your backpressure.
When a Subscriber
is notified by a Producer
that there is a new
element available in Combine, the Subscriber
is expected to return a
Subscribers.Demand
. By doing so, the Subscriber
is indicating to
the Producer
how many more elements it’s willing to accept.
✅ ✅ ✅ ✅ ✅ ✅ ✅
Combine accounts for backpressure at its core.
✅ ✅ ✅ ✅ ✅ ✅ ✅
The number of elements a subscriber is willing to accept can be
effectively infinite (.unlimited
), or a specific number
(.max(1)
). In Lucy’s case, she may return .max(3)
, knowing she
can do about three chocolates at a time. In the case of our deposit
button handler, we may return .max(1)
, thereby preventing more
than one deposit.
[Still the] Same as It Ever Was
Just like the difference in error handling, there’s not really a clearly right or wrong answer between RxSwift’s and Combine’s approaches. Both projects have made design decisions, all of which are completely reasonable. To me, this is what makes engineering fun: balancing the pros and cons to different approaches and coming up with a solution that makes the best possible trade offs.
As with the discussion on error handling, the Combine approach leads to a little bit more bookkeeping, but a more robust solution.
In my experience, I can’t say I’ve had many occasions where I’ve
thought “oh man, I wish RxSwift had backpressure”. However, I can
pretty easily eliminate backpressure from Combine by simply using
Demand.unlimited
whenever I’m asked for a Demand
. Thus, this
design decision I find less bothersome than the choices Combine
makes about error handling. Handling backpressure is far less of
a bookkeeping burden.
Wrapping Up
In my next post, I’ll summarize the differences between the projects, and give a[n initial] ruling on what I plan to do in Vignette, and other projects going forward.
Yesterday’s post aside, we’ve spent the last several days discussing RxSwift and Combine:
- How do we get to
Observables
? - What is RxSwift anyway?
- Where are UIKit bindings in Combine?
- What’s new in Seed 2? Can KVO save us?
In Monday’s post, I said the following:
In order to discuss 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
We covered the first — bridging to non-reactive classes — in Monday’s and Tuesday’s posts. Today, let’s discuss error handling.
Going Back to the Beginning
If you recall, in our first post, we built up our own Observer
type by hand. This is where we landed:
protocol Observer {
func onComplete()
func onError(Error)
func onNext(Element)
}
Note, in particular, the way errors are handled:
func onError(Error)
Herein lies the dramatic difference between RxSwift and Combine.
What even is an error, anyway?
In Swift, all errors can be eventually traced back to a single
protocol Error
. This protocol is basically just a marker;
it doesn’t carry with it any particular functionality. This is
wonderful, because it makes it exceptionally easy to quickly
create a class
, struct
, or even an enum
that is a valid,
throw
able error.
When it comes to Observable
s/Publisher
s, there are two basic
approaches that API designers can choose between:
- Assume every stream can end in an
Error
, and not get specific about what kind ofError
it is. - Specify up front precisely what kind of
Error
can be emitted
There are benefits to each approach:
- Assuming any
Error
means you don’t have to be bothered with specifying a specificError
type every time you create a stream, much less creating semantic errors for every stream. - Specifying specific
Error
s means you always know the exact kind ofError
that could end a stream. This leads to better local reasoning, and the errors are more semantically meaningful.
Naturally, there are also drawbacks:
- Assuming any
Error
means literally anyError
could end any stream. You never really know what could pop out at the end of a stream until it happens. - Specifying specific
Error
s means you must be explicit, always, about what could end every stream. This is a not-inconsequential amount of overhead and bookkeeping.
Error Handling in RxSwift
RxSwift takes the first approach.
In RxSwift, every stream can error with any kind of Error
.
Naturally, the advantage of this is a dramatically reduced
amount of bookkeeping. One doesn’t need to worry about specifying
what error type may be emitted, because the answer is assumed:
any Error
can be emitted.
However, that also makes it a little harder to understand what
can go wrong, or perhaps, how it can go wrong. Literally
every error in Swift is also an Error
. Thus, it is —
from a type system perspective — possible for any
Error
to be emitted from any stream.
Error Handling in Combine
It’s easy to guess what happens on the other side of the fence.
In Combine, every Producer
(/Observable
) must specify
the exact Error
type up front.
This leads to a bit more bookkeeping; any time you create a
Producer
you must also specify what type of Error
that
Producer
could emit. The advantage here is that you know
exactly what kind of Error
may be emitted. If not a precise
type, at worst, a type hierarchy where the base is known.
That improves both local reasoning, as well as semantic meaning.
Furthermore, one can cheat a couple of different ways. There
is nothing stopping you from specifying the Error
type as…
well… Error
. That puts us basically in the world of
RxSwift: a stream that can emit any Error
.
Additionally, one can really really cheat by using a
special type in Swift: Never
.
Never
is a special type that, by design, can never be
instantiated. (Behind the scenes it is an enum
eration
that has no case
s). If the error type in a Producer
is
Never
, guess how often that Producer
can error? Not once.
Not even a little bit.
Which is better?
This is a case wherein the delta is simply that: a difference. Sitting here today, I can’t say whether one is better or worse than the other. The lazy developer in me isn’t overjoyed by the thought of all the additional housekeeping in Combine. However, the purist in me admires the clarity of specifying specific errors.
If I were to guess, I’d assume that I’ll start by complaining and moaning about the additional bookkeeping, and then eventually come around to the clarity of Combine’s approach.
Next Steps
In the next — and possibly last — post, I’ll explore the final of the three major differences I’ve spotted between Combine and RxSwift: backpressure.
Within just a couple hours of posting yesterday, I had some new information to consider. I wanted to call attention to it before continuing on our RxSwift/Combine comparison.
New Documentation
Yesterday afternoon Apple released iOS 13 beta 2; with it came some new documentation. This new document, Receiving and Handling Events with Combine, is a brief overview of how one can, well, receive and handle events in Combine.
The introduction is good, and demonstrates how one can get a value out of a text field and store it in a custom model object. The documentation also demonstrates the use of operators to do some slightly more advanced modification of the stream in question.
Sample Code
Cutting to the end of the document, here’s the sample code Apple shared:
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.assign(to: \MyViewModel.filterString, on: myViewModel)
I have… a lot of problems with this.
I’m Notifying You I Don’t Like This
Most of my problems with this code are in the first two lines:
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
NotificationCenter
is a sort of application (or even system)
bus, where lots of things can all drop data, or pick up pieces of
data that are flying by. It’s a sort of all-things-to-all-people
kind of solution, and that’s by design. There are lots of instances
where you may want to be able to figure out if, say, the keyboard
has just been shown or hidden. NotificationCenter
is a great way
to spread that message around within the system.
I find NotificationCenter
to be a bit of a code smell.
There are absolutely times where I use NotificationCenter
, and
in fact, there are times [like the keyboard notification above] that
NotificationCenter
is the best possible solution for a
problem. However, all too often I feel like using NotificationCenter
is the most convenient solution.
It’s extremely easy to drop something on the NotificationCenter
bus,
and to pick it up somewhere else on the other side of your app.
Furthermore, NotificationCenter
is “stringly” typed, which is
to say, it’s easy to make errors about what notification you’re trying
to post or listen for. Swift does its best to make this a bit better,
but ultimately it’s still NSString
under the hood.
An Aside about Key-Value Observation
A popular way to get notifications about things changing in different pieces of code is a technology that has been around for a long time in Apple platforms: key-value observation. Key-value observation is described by Apple as such:
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
I also noticed, thanks to a tweet from Gui Rambo, that Apple has added bindings for KVO to Combine in this new beta. That means that a lot of my gripes about there being no equivalent to RxCocoa in Combine may have gone away. If I could use KVO, that would probably obviate much of the need for “CombineCocoa”, so to speak.
I got to working on a sample of my own that used KVO to get the value
out of a UITextField
and simply print()
it to the console:
let sub = self.textField.publisher(for: \UITextField.text)
.sink(receiveCompletion: { _ in
print("Completed")
}, receiveValue: {
print("Text field is currently \"\($0)\"")
})
Good to go, right?
I had forgotten a very inconvenient truth:
UIKit, by and large, is not KVO-compliant.
😭
That means without KVO support, my idea doesn’t work. My testing
confirmed it: my code never print()
ed anything as I entered
text in the field.
Thus, my fantasy of KVO eliminating much of the need for UIKit bindings was incredible, but short-lived.
Cancellation
The other problem I have with Combine is that it still isn’t terribly
clear to me where/how Cancellable
objects are supposed to be
cleaned up. It seems that we’re supposed to keep a copy of these as
instance variables. I don’t recall having read any official documentation
about cleanup though.
(If you have, do let me know, please!)
In RxSwift, we had the awfully-named-but-ultimately-convenient
DisposeBag
. It’s trivial to create a CancelBag
in Combine, but
I’m still not 100% clear if that’s really the best approach.
Highs and Lows
All told, quite a lot was added to Combine in beta 2, and I am very excited to see what comes in future betas. Nonetheless, none of these new goodies have really swayed my opinions… yet.
In my next post, we’ll go ahead and cover how error handling works in RxSwift versus how it works in Combine, and the plusses and minuses of both approaches.
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.