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.
In yesterday’s post, we walked through how one could take “Enumerable
”,
(or, really, Sequence
) and “Enumerator
”
(really, Iterator
), and turn them into Observable
and
Observer
. These are the two types that underpin most everything in
RxSwift.
In this post, let’s explore a little more about what RxSwift is, and what it isn’t.
Let’s Make a Sandwich
When people colloquially refer to “RxSwift”, they’re often referring to an entire group of projects and technologies. When writing an iOS app, it is certainly possible to use only RxSwift, but that’s like making a sandwich only of bread.
In reality, the beauty of RxSwift from an app developer’s perspective is not the RxSwift “bread”, but rather the meats and condiments inside. More directly, RxSwift is fine, but interaction with things like user interfaces and system frameworks is what makes RxSwift really shine.
One of my favorite uses of RxSwift is to interact with user interfaces.
The taps of a UIButton
are an excellent example of something that can
be exposed as an Observable
.
However, using only RxSwift, there isn’t a terribly straightforward
way to expose a button tap as an Observable. It’s certainly doable, but
would require creating an entire object just to be the receiver of the
button’s .touchUpInside
action
. That’s a lot of housekeeping to
get to one button’s tap.
Thankfully, many of these menial tasks are already taken care of. Instead of having to worry about writing an entire object to worry about a button tap, you can leverage a project that’s built upon RxSwift.
RxCocoa
RxCocoa is sort of part of RxSwift, though it’s sort of not.
It’s a separate target, and must be import
ed on its own, but the source
lives within the RxSwift repository.
It’s RxCocoa that brings all of these convenient bindings to the table. Thus, instead of having to do a whole bunch of housekeeping in order to get to a button tap, one can just do this:
let disposeBag = DisposeBag()
let button = UIButton()
button.rx.tap
.subscribe(onNext: { _ in
print("Tap!")
})
.disposed(by: disposeBag)
That is so nice, and so convenient.
Furthermore, there are equivalent bindings for mostly anything an
average iOS developer runs into in a normal application. RxCocoa has
bindings for UITabBar
’s selectedItem
. For
UIProgressView
’s progress
. For UITextField
’s
text. The list goes on and on and on.
Let’s Join a [RxSwift]Community
RxCocoa can’t be all things to all people, though. However, the beauty of open-source is that others can join in, help, and share their code.
Enter RxSwiftCommunity.
RxSwiftCommunity is, naturally, a community-led project to extend RxSwift
and RxCocoa to cover what those projects do not. A phenomenal example of
this, that I’ve used in Vignette, is RxGesture. RxGesture
exposes many gesture-based events by way of Observable
s. For example:
let someView = UIView()
let observable: Observable<UITapGestureRecognizer> =
someView.rx.tapGesture { (recognizer, _) in
recognizer.numberOfTapsRequired = 3
}
.when(.recognized)
Just like before, it’s certainly possible to do this by hand, but it is so much nicer to be able to just grab an open-source solution to do it for you. An open-source solution that has been used many many times, and thus is far better tested than any bespoke solution would be.
Furthermore, there are a ton of RxSwiftCommunity projects. To call out just a few:
- RxDataSources
A personal favorite of mine, which makes usingUITableView
andUICollectionView
incredibly easy - RxRealm
- RxMKMapView
Finally, newest and perhaps most interestingly:
- RxCombine
Currently empty, but perhaps an attempt at an RxSwift ↔ Combine bridge?
Speaking of Testing
Another incredible feature of RxSwift, and peer to RxCocoa, is RxTest.
RxTest is a wonderful suite of tools that allows you to easily test
code based on Observable
s. This includes simulating events on a stream,
at precise virtual times, in order to observe their results on the system.
I cover a lot of RxTest in my fifth and final RxSwift Primer post. Suffice it to say, RxTest makes it [almost] enjoyable to write unit tests for RxSwift-based code.
All Together Now
RxSwift in and of itself is impressive, but it is an empty sandwich. While it may be made of the most delicious bread known to man, it’s still just bread. Without the meat that is RxCocoa, the cheese that is RxCommunity, and the condiments that are RxTest, it’s just not the same.
Next week, I’ll spend some time comparing the API surface area of Combine to that of RxSwift, as well as discuss some core differences in the design of the two projects.
I’ve been preaching the gospel about RxSwift for a year and a half now. RxSwift took me quite a while to get my head around, but once it clicked, there was no going back for me. I now have the shiniest hammer in the world, and I’ll be damned if everything doesn’t resemble a nail.
A little over a week ago, at WWDC, Apple unveiled their
Combine
framework. At a glance, Combine
seems like little more than
a first-party take on RxSwift. Before I can really get into what I do
and don’t like about it, we need to understand what problem Combine is setting
out to solve.
Reactive Programming? What now?
The ReactiveX community — the community which RxSwift is a part of — summarizes itself as follows:
An API for asynchronous programming with observable streams
And further:
ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming
Um… k. 👌🏻
So what the hell does that really mean?
A Foundation
In order to really understand what reactive programming is about, I find it helpful to understand how we got here. In this post, I’ll describe how one can look at existing types in any modern object-oriented programming language, twist them around, and land on reactive programming.
This post gets in the weeds fast and isn’t absolutely necessary to understanding reactive programming.
However, I do think it’s a fascinating academic exercise, especially in how strongly typed languages can lead us down the path to new discoveries.
So, feel free to wait until my next post if this goes too far off in the weeds for you.
Enumerables
The “reactive programming” that I know was borne from my old language of choice, C#. The whole premise, distilled, is fairly simple:
What if instead of enumerables where you pull values out, you instead got values pushed to you?
This push rather than pull idea was best described to me in an incredible video with Brian Beckman and Erik Meijer. The first ~36 minutes is… over my head, but starting at around 36 minutes, things get really interesting.
In short, let’s re-define the idea of a linear group of objects in Swift, as well as an object that can iterate across that linear group. We can do so by defining these fake Swift protocols:
// A linear group of objects; you could easily imagine this
// being backed by an Array.
protocol Enumerable {
associatedtype Enum: Enumerator
associatedtype Element where Self.Element == Self.Enum.Element
func getEnumerator() -> Self.Enum
}
// An object that can walk across a linear group of objects.
protocol Enumerator: Disposable {
associatedtype Element
func moveNext() throws -> Bool
var current: Element { get }
}
// We may eventually need to clean up our
// Enumerator; it could be operating on files,
// or a network resource. This is how we do so.
protocol Disposable {
func dispose()
}
Duality
Let’s flip all of those, or make their duals. So where data was coming out, we put data in. Where data was going in, we pull data out. This sounds funny, but bear with me.
Duality of Enumerable
Starting with Enumerable
:
// Exactly as seen above.
protocol Enumerable {
associatedtype Element where Self.Element == Self.Enum.Element
associatedtype Enum: Enumerator
func getEnumerator() -> Self.Enum
}
protocol DualOfEnumerable {
// Enumerator has:
// getEnumerator() -> Self.Enum
// Which could be rewritten as:
// getEnumerator(Void) -> Enumerator
//
// Thus, we could summarize:
// IN: Void; OUT: Enumerator
// getEnumerator(Void) → Enumerator
//
// Thus, we are taking in Void and emitting an Enumerator.
// For the dual of this, we should take IN whatever the
// dual of an Enumerator is, and emit Void.
// IN: Dual of Enumerator; OUT: Void
func subscribe(DualOfEnumerator)
}
Again, since getEnumerator()
took in Void
and emitted an
Enumerator
, we are instead taking in [the dual of] Enumerator
and emiting/returning Void
.
I know this is weird. Stick with me here.
Duality of Enumerator
So what is DualOfEnumerator
then?
// Exactly as seen above.
protocol Enumerator: Disposable {
associatedtype Element
// IN: Void; OUT: Bool, Error
func moveNext() throws -> Bool
// IN: Void; OUT: Element
var current: Element { get }
}
protocol DualOfEnumerator {
// IN: Bool, Error; OUT: Void
// The previously thrown Error we will ignore for a moment
func enumeratorIsDone(Bool)
// IN: Element, OUT: Void
var nextElement: Element { set }
}
Now, a few problems here:
- Swift doesn’t have the concept of a set-only property
- What happened to the
throws
onEnumerator.moveNext()
? - What about
Disposable
? What happens with that?
To fix the set-only property, we can treat a set-only property as
what it really is: a func
. Thus, let’s slightly tweak our DualOfEnumerator
:
protocol DualOfEnumerator {
// IN: Bool; OUT: Void, Error
// The previously thrown Error we will ignore for a moment
func enumeratorIsDone(Bool)
// IN: Element, OUT: Void
func next(Element)
}
To fix the throws
, let’s break out the error that could happen in
moveNext()
and treat it as its own separate func
called error()
:
protocol DualOfEnumerator {
// IN: Bool, Error; OUT: Void
func enumeratorIsDone(Bool)
func error(Error)
// IN: Element, OUT: Void
func next(Element)
}
There’s one other change we can make: look at the signature for when we’ve finished enumerating:
func enumeratorIsDone(Bool)
Presumably we’d have something like this over time:
enumeratorIsDone(false)
enumeratorIsDone(false)
// Now we're finally done
enumeratorIsDone(true)
At that point, why not just simplify things and only call
enumeratorIsDone
when… things are done? We can take
that approach, and simplify the signature:
protocol DualOfEnumerator {
func enumeratorIsDone()
func error(Error)
func next(Element)
}
Cleaning Up Our Mess
Finally, what about that Disposable
? What do we do with that?
Well, since Disposable
is a part of the type Enumerator
,
when we get the dual of Enumerator
, perhaps it shouldn’t be
on Enumerator
at all. Instead, it should be a part of
DualOfEnumerable
. But where?
The place where we are taking in the DualOfEnumerator
is here:
func subscribe(DualOfEnumerator)
If we’re taking in the DualOfEnumerator
, then shouldn’t the
Disposable
pop out?
Thus, here’s our final dual of everything:
protocol DualOfEnumerable {
func subscribe(DualOfEnumerator) -> Disposable
}
protocol DualOfEnumerator {
func enumeratorIsDone()
func error(Error)
func next(Element)
}
By Any Other Name
Okay, one more time, this is what we have:
protocol DualOfEnumerable {
func subscribe(DualOfEnumerator) -> Disposable
}
protocol DualOfEnumerator {
func enumeratorIsDone()
func error(Error)
func next(Element)
}
Let’s massage these names a bit.
Starting with DualOfEnumerator
, let’s use some slightly better
function names, which indicate these things just happened:
protocol DualOfEnumerator {
func onComplete()
func onError(Error)
func onNext(Element)
}
Cool, looking better and more consistent already.
How about these type names though? They’re straight garbage.
Let’s change them a bit.
- The
DualOfEnumerator
is something that pays attention to what happens with a linear group of objects. You could say it observes the linear group. - The
DualOfEnumerable
is the subject of that attention. It’s the thing we’re observing; thus you could say it is observable.
With this in mind, let’s do some renaming:
protocol Observable {
func subscribe(Observer) → Disposable
}
protocol Observer {
func onComplete()
func onError(Error)
func onNext(Element)
}
Whoa 🤯
We just built the two foundational objects in RxSwift; you can see the real ones here and here.[1])
These two types are what drive the basis for RxSwift and reactive programming.
About Those “Fake” Protocols
The two “fake” protocols
I described above aren’t really fake at all.
In reality, there are analogous types in Swift:
y tho?
So why bother?
So much of modern development — particularly app development — is about being asynchronous. The user has unexpectedly tapped this button. The user has unexpectedly changed this segmented control. The user has unexpectedly selected this tab. This web socket just unexpectedly gave us new information. This download is unexpectedly, and finally, complete. This background task has just unexpectedly finished. The list goes on and on.
There are so many ways to handle these sorts of things in today’s CocoaTouch world:
- Notifications
- Callbacks
- Key-Value Observation
- Target/action
Imagine if all of those things could be reflected by one unified interface. An interface that can reflect pretty much any kind of asynchronous data or event in your entire app?
Now imagine there was an entire suite of functions that allow you
to modify these streams, changing them from one type to another, extracting
information from within the Element
s, or even combining them with other streams?
Suddenly, there’s an entire new, universal toolbox at our disposal.
And lo, we’re back where we started:
An API for asynchronous programming with observable streams
That’s exactly what makes RxSwift so powerful. And, similarly, Combine
as well.
What’s Next
If you’d like to see more about RxSwift in action, I encourage you to read my five-part blog series from late 2016. It covers the creation of the world’s dumbest CocoaTouch app, and then converts it step by step to RxSwift.
In one [or more] future post[s], I’ll cover why many of the techniques used in
my RxSwift primer are not applicable to Combine
, and compare and
contrast Combine
with RxSwift.
Note in the case of
Observer
, the threeon()
functions are combined into oneon(Event)
, whereEvent
is anenum
that specifies if the event is a completion, next, or error. ↩
First and most importantly, THANK YOU to anyone who has bought Vignette, told a friend about it, given it a spin, or otherwise had a think about it. The response to Vignette has far surpassed my wildest dreams and I have you to thank for that. 💙
A week after its [quiet] release, and just under a week after its public release, I wanted to spend a moment to take stock of the last several days.
Vignette Updates
By the time I started my full-court press… press… Vignette was already on its second version. Since that time, I’ve released:
- 2019.3
- Full resolution images from Twitter
- Fix a problem where
Instagram
was not recognized when it had a trailing space - Fixed an accidental bait-and-switch
- Improved security
- 2019.4
- Dramatically improved Facebook support; the following are now supported:
- The hot garbage original requirement of
fb://profile/1234567
casey.liss
https://www.facebook.com/casey.liss
https://facebook.com/casey.liss
- The hot garbage original requirement of
- Fixed a bug wherein default Twitter images were suggested
- Vignette keeps the screen on when plugged in to prevent sleep-related issues
- Dramatically improved Facebook support; the following are now supported:
Forthcoming:
- 2019.5 — currently in testing
- For all services but Gravatar, Vignette will now look at any URL it can find on a contact, rather than only looking in
Social Profiles
- This will also get around
Social Profiles
not existing for Exchange users.
- This will also get around
- Improved networking detection
- The app will automatically start a search on launch if Wi-Fi is detected
- If no Wi-Fi is available, users can elect to perform a search over cellular
- New close button in the selector modal
- Fix for a layout issue in the onboarding screen
- Fix for very long usernames in the selector modal
- For all services but Gravatar, Vignette will now look at any URL it can find on a contact, rather than only looking in
- 2019.6 — currently in development; no guarantees here!
- Add support for Github profile pictures
- Fix for some of the first onboarding screen text getting cut off
- Better Facebook default image detection
- Accessibility escape gesture for the old avatar preview modal
- Under-the-hood improvement to the way I’m managing queues
2019.5 will be released once I get it in front of my Test Flight users for a couple days. It’s currently sitting and waiting for review from Apple. 🙄
2019.6 will probably be a mid-June release, if all goes to plan.
Revenue
I’d love to tell you I’m diving into my Scrooge McDuck swimming pool, but… App Store Connect isn’t really telling me much of anything at the moment. 😭

I’ve reached out to Apple in a couple of different ways to attempt to get this fixed, but nothing yet.
To be honest, I don’t intend to release revenue figures, but I am hoping to call a little more attention to this extremely frustrating App Store Connect bug.
Press
I’ve been extremely lucky to get some really incredible press coverage about Vignette. Mostly for my own posterity, I wanted to capture the articles I’ve found/seen:
- MacStories — Vignette: Easily Update Your Contact Photos Without Sacrificing Privacy
- Cult of Mac — Vignette adds contact photos to your faceless friends
- 512 Pixels — Vignette Makes Updating Contact Photos Easy and Secure
- 9to5Mac — Vignette for iOS easily personalizes contact photos using social media
- TechCrunch — Vignette is a handy new app that keeps your iOS contact photos up to date
- Connected — #244: This is Not Propaganda
- Mobile App Daily — Vignette App Launched For Apple Users To Personalise Contacts Pics Via Social Media
- The Mac Observer — Vignette App Lets You Update Your Contact Photos Privately
- MacMagazine — Novo app garante que todos os seus contatos do iPhone tenham fotos atualizadas
- Good Morning, RVA — Long Weekend, Fare Evasion, and a New App
- iGeneration — Vignette donne un visage à vos fiches dans Contacts
- NU.nl — Apps van de week: Vignette en The Gardens Between
- iPhone-Ticker — Vignette: App sucht Kontaktbilder in sozialen Netzen
- iMagazine — Vignette – automatycznie przeszukuje internet i dodaje avatary do Twoich kontaktów
General Thoughts
Overall, I really am overjoyed with how the launch has gone. It hasn’t been problem-free, but it’s gone so much better than I expected. I’m hopeful to get 2019.5 out the door prior to WWDC, but either way, I suspect I have a busy summer of iOS 13 updates ahead of me.
As with all ideas, it starts with a question:
Could I get new pictures for all my contacts on my iPhone off Gravatar?
That was early February. I was just goofing off and seeing if I could make that work at all.
Then more questions came:
Could I do this for Twitter as well? For Instagram? For Facebook?
About three months later, Vignette is here.
Introducing Vignette

Vignette allows you to add photos to your contacts by searching public social media profiles. If multiple options are found, it’s easy to select the one you wish to use. If the existing image is special or more representative of the contact, it can be kept.
By default, within a few minutes, you can add tens or even hundreds of images to your contacts.
In summary, this is the change that Vignette will enable:

Vignette’s development was driven by a few core tenets. I have a clear idea of what I’d like Vignette to be. These tenets drove its development:
- Privacy is paramount
All the processing is done on-device; this isn’t the sort of app where your contacts are uploaded en masse to some server, and out of your control. - You are the customer
It’s gross to steal users’ contacts and sell that data. My customer is you, not some business I’m selling your contacts to. - Keep it simple
I already have lots of ideas for how to make Vignette more robust and even more indispensible. Over time, I hope to add a bunch of those features. But Vignette is designed to be a tool, not Facebook.
Making Money
Vignette allows you to scan your contacts and see what it can find for free. If you wish to actually save these updates to your contact list, you must pay for a one-time in-app purchase. That purchase costs $4.99, is not a subscription, and is the only in-app purchase.
My hope is to keep developing and improving Vignette over time; that is made possible by the financial support of in-app purchase. Well-wishes, kudos, congratulations, and word-of-mouth all help quite a bit, but they don’t pay the bills. :)
Using Vignette
Vignette works by scanning your contacts and seeing what information it can amass about their social media presence. That means that Vignette is only as good as the information you provide to it.
Apple’s Contacts
app actually allows you to provide this information, even
though it isn’t entirely obvious at first:

Vignette will look at the following fields:
Email
is used for GravatarTwitter
Facebook
- A custom network called
Instagram
Over time, I hope to make discovery for these existing services more robust, and potentially begin to support other services, as well. That said, for now, my priority is to make all this possible without requiring you to log in. I much prefer having Vignette only use social networks anonymously to prioritize privacy.
Why Now?
Vignette is the first new project I’ve undertaken since going independent. I’ve been working on the things I had already been doing, like Casey on Cars, but Vignette is the first project both conceived and completed while indie.
I’ve been working on it feverishly for months, but a recent report really lit a fire under me. I really wanted to get Vignette out the door before WWDC, and I’m overjoyed to have made it, with over a week to spare.
Some Initial Coverage
Naturally, I have and will be talking about this on my podcasts. Myke and I have recorded episode 157 of Analog(ue) where we’ve discussed the lead-up to the launch.
Additionally, [the currently forthcoming] episode 327 of ATP will surely include some commentary.
Thank You
Please download Vignette and see what you think of it. If you’re willing, I’d love it if you’d buy the in-app purchase. Or tell your friends about Vignette. Or both!
Additionally, my thanks to Ben McCarthy, Daniel “Jelly” Farrelly, and Ste Grainer — among others — for their noteworthy and incredibly helpful contributions to Vignette.
One of my favorite local activities, every other Saturday, is to go to Cars and Coffee. Recently, our local paper had a nice piece about it:
A stunning new Acura NSX might be followed by a more pedestrian mid-aught Volkswagen Golf GTI or a midcentury representative of American muscle. A made-to-order Ford GT supercar mingles with its ubiquitous cousins – a bevy of Ford Mustang GTs that might be worth only a tenth of the supercar’s list price.
The thread that unites all attendees is pride in their vehicles and a desire to share them with other people who “get it.” Enthusiasts of every background are brought together by the camaraderie of the local car culture.
I’ve often documented my trips to Cars and Coffee on Instagram in the past. I really love taking the family to go check out the cars and just generally enjoy a Saturday morning together.
Perhaps it’s memories of going to Marcus Dairy when I was in high school, or perhaps it’s me trying to share with my kids what my dad shared with me. But one way or the other, Cars and Coffee has been an integral part of my family for years. I’m pleased to see it get some local recognition.
This week I had a blast joining my good friends Stephen Hackett and Jason Snell on this week’s episode of Download.
On this week’s show, Jason and Stephen discussed some new Fitbits, Apple hires in Qualcomm’s area of expertise, the recent Facebook news. Then I join the pair for some insights about the Geneva Motor Show.
I always cherish a time I can Stephen or Jason on a show, and any time I can talk about cars, so this was a win/win. 🎉