Professional Documents
Culture Documents
Practical Combine
Practical Combine
Practical Combine
Chapter overview 8
Chapter 1 - Introducing Functional Reactive Programming . . . . . . . . . . . . . . 8
Chapter 2 - Exploring publishers and subscribers . . . . . . . . . . . . . . . . . . . 8
Chapter 3 - Transforming publishers . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Chapter 4 - Updating the User Interface . . . . . . . . . . . . . . . . . . . . . . . . 9
Chapter 5 - Using Combine to respond to user input . . . . . . . . . . . . . . . . . 9
Chapter 6 - Using Combine for networking . . . . . . . . . . . . . . . . . . . . . . 10
Chapter 7 - Wrapping existing asynchronous processes with Futures in Combine . . 10
Chapter 8 - Understanding Combine’s Schedulers . . . . . . . . . . . . . . . . . . 11
Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions . . . . . 11
Chapter 10 - Debugging your Combine code . . . . . . . . . . . . . . . . . . . . . . 11
Chapter 11 - Testing code that uses Combine . . . . . . . . . . . . . . . . . . . . . 12
Chapter 12 - Driving publishers and flows with subjects . . . . . . . . . . . . . . . 12
Chapter 13 - Where to go from here . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Transforming publishers 30
Applying common transformations to a publisher . . . . . . . . . . . . . . . . . . 30
Understanding the differences between map, flatMap and compactMap . . . . . . 35
Using compactMap in Combine . . . . . . . . . . . . . . . . . . . . . . . . . 35
Practical Combine
Donny Wals 3
Practical Combine
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Donny Wals 4
Practical Combine
Donny Wals 5
Practical Combine
Donny Wals 6
Practical Combine
better at it. Benedikt Terhechte and Vadim Drobinin for giving me feedback on the pre-release
edition of this book. Paul Hudson for helping me promote my book and for everything he does
to support folks in our community. Marin Todorov for reviewing my chapter on debugging and
his tool Timelane. Joe Fabisevich for asking me countless questions about this book, keeping
on my toes and helping me improve the book in several areas. And lastly, I want to thank my
fiancee, Dorien for always putting up with me when I decide to take on yet another writing
project.
If you find any mistakes, errors or inconsistencies in this book don’t hesitate to send me an
email at feedback@donnywals.com. I’ve put a lot of care and attention into this book but
I’m only human and I need your feedback to make this book the best resource it can be. Make
sure you also reach out if you have any questions that aren’t answered by this book even
though you hoped it would so I can answer your questions directly, or possibly update the
book if needed.
Cheers,
Donny
Donny Wals 7
Practical Combine
Chapter overview
Donny Wals 8
Practical Combine
more. We’ll wrap up the chapter by defining a custom operator on Publisher that bundles
a couple of other operators in a convenient wrapper.
Donny Wals 9
Practical Combine
Donny Wals 10
Practical Combine
Donny Wals 11
Practical Combine
Donny Wals 12
Practical Combine
By the end of this chapter, you should be able to rationalize choosing Combine in your projects,
and it should be clear what FRP is, and how it benefits you as a developer.
Donny Wals 13
Practical Combine
[1, 2, 3].map { $0 * 2 }
The preceding code takes an array of integers and calls map on it. This allows us to transform
the array of integers into something else. In this case by multiplying the argument that is
received by map (we use $0 as a short-form notation in Swift) and multiplying it by two. The
map function only operates on the array that it’s called on and instead of changing the existing
array, it returns a brand-new, mapped array. The closure passed to map acts as a function in
functional programming and it only operates on the arguments that it receives. This means
that we were able to create a new array with different contents than the original, without
performing any work that’s not encapsulated by a function.
A function that takes another function or closure as its parameter is called a higher-order
function in Functional Programming. A function that only operates on the arguments it
receives is called a pure function. These two terms are extremely important because they
form the basis for many of Combine’s features.
Since my goal is not to teach you Functional Programming, I will explain bits and pieces about
Functional Programming throughout the book where needed. Some explanations might be
simplified to make sure we don’t get lost in all the small details and nuances of Functional
Programming because I want to make sure that we focus on the goal which is to get you up
and running with Combine.
So at this point, you should have enough information to understand what the functional bit in
FRP means. So what does the reactive bit mean?
The reactive part of FRP means that we don’t operate on objects synchronously. Instead, we
compose functions and operations in such a way that whenever something happens and we
receive a new value of something, we perform operations on it, to get to a certain outcome. If
you consider the map example I showed you earlier, you can envision how that works. First, we
have an array with [1, 2, 3] as its contents and when the map is executed, it creates a new
array that has [2, 4, 6] as its contents. Simple enough. Everything runs synchronously.
Similar to how we can map over an array of known values, we can map over an array of
unknown values. Consider a scenario where values or events are emitted over time. We can
take each new value as it’s emitted, and we can transform it using a map until we have a result.
Let’s look at an example so you can get an idea of what I mean:
Donny Wals 14
Practical Combine
someButton.onTap.map { _ in
return Bool.random()
}.sink { didWin in
if didWin {
print("Congratulations! You won a price")
} else {
print("You didn't win! Better luck next time")
}
}
Tip: If you want to learn more about the preceding code, check out Chapter 5 - Using
Combine to respond to user input
The preceding code is an example where we take tap events that are emitted by a button when
a user taps this button. We call these events a stream. As you will learn later, everything in
FRP is considered a stream. You can think of a stream as an array of values where the values
are delivered over time. Whenever a new tap value is emitted, it is passed to map. The map
implementation that I just showed you ignores the value, and transforms it into a random
boolean, much like how we multiplied integers in the map example that I showed you earlier.
Finally, a message is printed based on the received boolean.
In Combine, we use the sink method to receive values. It’s okay to perform mutations or to
operate on the world that surrounds the sink because it is not part of the publisher chain. You
will learn much more about sink in Chapter 2 - Exploring publishers and subscribers.
Based on the examples above, you can say that FRP uses Functional Programming to react to
events that occur over time, or asynchronously.
Donny Wals 15
Practical Combine
Even though you haven’t learned any Combine yet, I want to show you an example of how
Combine can increase readability when comparing it to a traditional callback-based approach.
If you perform a network call without Combine, you might write code that looks something
like the following:
completion(.success(data))
}.resume()
}
While the code above isn’t complex, you can see how it would be hard to chain together several
data tasks because you would have to nest completion handlers deeper and deeper. And not
just that, there is a large amount of boilerplate code involved with this simple request. First,
we need to do some optional unwrapping in the completion handler for the request itself, and
then there is more boilerplate code involved where requestData(_:) is called because
the Result object needs to be unpacked. Let’s look at the same request in Combine:
Donny Wals 16
Practical Combine
.map(\.data)
.eraseToAnyPublisher()
}
Instead of handling error and success in both the completion handler that’s passed to
URLSession, and then again in the method that calls requestData, the completion
and error events are passed down Combine’s value stream. If you’re not yet sure what that
means, don’t worry. I will explain it all in-depth in the next chapter, Exploring publishers
and subscribers. Also note that instead of returning an object that has a generic Error as
its failure type, we can return a publisher that has a URLError as its failure type. This is
much more convenient for callers of requestData. All in all, Combine can help you get rid
of lots of boilerplate code which will help you focus on what matters; coding your app.
Also note that because Combine uses several small and pure functions called operators
to transform values as they are passed down the chain, you can perform lots of readable,
boilerplate-free transformations that almost read like normal sentences. Let’s briefly look at
the example below. Even though I haven’t explained Combine yet, I’m sure you have an idea
of what this code does:
requestData()
.decode(type: SomeModel.self, decoder: JSONDecoder())
.map { $0.name }
.sink(receiveCompletion: { _ in },
receiveValue: { print($0) })
If you weren’t sure, the code above calls the requestData method I showed you earlier, it
decodes the data returned by the request into a SomeModel, it maps over the result to extract
the name of the decoded data, and it then prints the name. You will learn more about all of this
in the chapters to come but I wanted to show you how readable a chain of Combine operators
can be, and how you can compose them together to create a chain of transformations that
make small, isolated changes to data.
Donny Wals 17
Practical Combine
Donny Wals 18
Practical Combine
In Summary
In this chapter, I have explained the basics of FRP to you. You learned that FRP borrows a
lot of principles from Functional Programming and that it uses pure functions and higher-
order functions to compose complex behaviors with several small operators. You saw some
examples, like how you could transform a button tap into a random boolean value using a
custom Combine publisher.
I also showed you how Combine can help you clean up callback-based APIs by hiding com-
plexity in small operators that each perform a little bit of work to get to the desired results.
Lastly, you learned about some of the similarities and differences between RxSwift and Com-
bine. In the end, they are very similar and you might even consider them to be two sides of
the same coin. I personally like Combine a lot, and I’m sure you will too.
In the next chapter, I will show you Combine’s publishers and subscribers, and you will learn
more about the bits and pieces of code that you saw in this chapter.
Donny Wals 19
Practical Combine
Once you understand all of the three topics listed above, you understand a huge part of the
Combine framework. In my opinion, its complexity often doesn’t lie in Combine itself. It lies in
how you can use it. And that’s the whole reason that a large part of this book is about practical
examples of Combine that you are likely to encounter in code that you will either find in the
wild or write yourself.
But we’re not quite there yet. There’s still some fundamental knowledge to be gained.
The preceding code converts an array to a publisher that emits the contents of this array one
by one. It will do this as soon as an object subscribes to it. I will explain this in-depth in the
Donny Wals 20
Practical Combine
next section.
If you look at the type of this publisher, you will find that it’s Publishers.Sequence<[Int],
Never>. This type signature tells us a lot about Combine. First, it tells us that Combine
contains an object called Publishers and that it likely defines several publishers. We can
explore this by looking at the documentation for Publishers.
If you do this, you will find that Publishers is an enum and that it’s “A namespace for types
that serve as publishers.”. In other words, this enum contains a lot of Combine’s built-in publish-
ers. The Sequence publisher is one of these built-in publishers. If you scroll through the list of
publishers you will find that publishers like Publishers.Map and Publishers.Filter
exist. If you’re familiar with Swift’s map and filter functions, you probably already know
what these publishers do. If you’re not familiar with these functions, or you’re not sure what
the Publishers.Map and Publishers.Filter publishers might do, don’t worry. I will
explain all of them in the next chapter.
If you look at the documentation for the Publishers.Sequence publisher, you will find
that it’s “A publisher that publishes a given sequence of elements.”. If you look at its type signa-
ture you’ll see that it takes an object that conforms to Sequence as its first generic argument,
and an Error as its second argument. This shouldn’t be too surprising based on the type
of publisher we created in the example earlier. What’s more interesting is that Publish-
ers.Sequence conforms to the Publisher protocol. If you look up this protocol in the
documentation, you’ll find that it provides a ton of functionality but what’s important for now
are Publisher’s associated types: Output and Failure.
Every publisher in Combine has an Output and a Failure. The Output of a publisher
is the type of value that it produces. For the Publishers.Sequence publisher that we
created earlier, the Output is Int. This means that subscribers of a sequence publisher
will receive Int objects as their input. Even though the publisher itself takes a Sequence
as its generic argument, it produces single elements as its output. The failure type of the
Publisher.Sequence from before is Never. This means that this publisher can only
complete successfully. It never emits error events.
In FRP, it’s common to reason about publishers and operations like map in a Marble Diagram.
The following image shows an example of a marble diagram that resembles the publisher
created in the code above.
Donny Wals 21
Practical Combine
I wouldn’t be surprised if you’ve been reading this section up until now, wondering when
you would ever use a publisher like the one I just defined. This book is all about getting you
to use Combine in practice, after all. The answer is somewhat unsatisfying. I don’t think it’s
likely that you will be creating publishers of sequences like we just did regularly. A simple
publisher like that does a fantastic job of showing what a publisher is though, and it allowed
us to explore what a publisher is.
When you start using Combine, you will find that Apple has created a bunch of publishers
that are defined as extensions of objects that are available in the UIKit and Foundation
frameworks. One example in the previous chapter was a publisher for a URLSession data
task:
Donny Wals 22
Practical Combine
Donny Wals 23
Practical Combine
The sink method that’s used in the preceding code is defined as an extension on Publisher.
This means that we can use sink to subscribe to every possible publisher in Combine be-
cause all publishers must conform to the Publisher protocol. The sink method takes two
closures. One is called for every value that is emitted by the publisher, and the other is called
after the publisher has emitted its last value. If you were to paste the preceding code into a
playground, you will find the following output:
received a value: 1
received a value: 2
received a value: 3
publisher completed: finished
The completion event that is sent to the receiveCompletion closure has Sub-
scribers.Completion<Self.Failure> as its type. This event is very similar to
Swift’s Result type, except its success case is simply .finished without an associated
value. If you’d want to check for errors in the receiveCompletion closure, which you
should, you could use a switch and the following code:
Donny Wals 24
Practical Combine
If you’ve been paying attention, you might wonder why we’d check for errors in the preceding
code. This specific publisher never emits an error because its Failure type is Never! First
off, kudos to you. That’s a sharp observation. And second, you’re right! In fact, sink comes
with a special flavor for publishers that have Never as their Failure type. It allows us to
omit the receiveCompletion closure:
This shorthand version of sink only works for publishers that never fail. If your publisher
can fail, you are required to handle the completion event using a receiveCompletion
closure.
The second very convenient built-in subscriber is assign. The assign subscriber is also
defined on Publisher and it allows us to directly assign publisher values to a property on
an object:
The preceding code creates a publisher that emits strings. We use assign to directly assign
every published string from this publisher to the user object’s email property. This can
be very convenient but it comes with some rules. The assign method requires that the key
path that we want to assign values to is a ReferenceWriteableKeyPath. This pretty
much means that the key path must belong to a class. For the preceding example, I used the
following User class:
class User {
var email = "default"
}
So while the assign subscriber is very convenient, it’s not always feasible to use.
Donny Wals 25
Practical Combine
let myNotification =
,→ Notification.Name("com.donnywals.customNotification")
func listenToNotifications() {
NotificationCenter.default.publisher(for: myNotification)
.sink(receiveValue: { notification in
print("Received a notification!")
})
NotificationCenter.default.post(Notification(name: myNotification))
}
Donny Wals 26
Practical Combine
listenToNotifications()
NotificationCenter.default.post(Notification(name: myNotification))
The preceding code snippet creates a notification center publisher that listens for a specific
notification. When a notification is received, a message is printed to the console. When
you run this code in a playground, you will find that the "Received a notification!"
text is only printed once. This happens because the first notification is posted inside of the
listenToNotifications function. The AnyCancellable that is returned by sink
still exists at that time. The second notification is posted outside of the listenToNoti-
fications function. Even though it’s posted after the function runs, the notification is not
received by the subscriber because the AnyCancellable is already torn down.
You can fix this by holding on to the AnyCancellable outside of the function body. One way
to do is by assigning the AnyCancellable to a property outside of the function scope:
let myNotification =
,→ Notification.Name("com.donnywals.customNotification")
var subscription: AnyCancellable?
func listenToNotifications() {
subscription = NotificationCenter.default.publisher(for:
,→ myNotification)
.sink(receiveValue: { notification in
print("Received a notification!")
})
NotificationCenter.default.post(Notification(name: myNotification))
}
Donny Wals 27
Practical Combine
let myNotification =
,→ Notification.Name("com.donnywals.customNotification")
var cancellables = Set<AnyCancellable>()
func listenToNotifications() {
NotificationCenter.default.publisher(for: myNotification)
.sink(receiveValue: { notification in
print("Received a notification!")
}).store(in: &cancellables)
NotificationCenter.default.post(Notification(name: myNotification))
}
Every AnyCancellable has a store(in:) method. This method takes an inout pa-
rameter which means that this method can append an AnyCancellable to the Set that
you pass it. In this case a set of AnyCancellable objects.
When a publisher completes and you have a subscription (AnyCancellable) for that pub-
lisher stored in a set or property, the AnyCancellable is not deallocated automatically.
Typically, you don’t need to worry about this. The publisher and subscription will usually
do enough cleanup to prevent any major memory leaks, and the objects that hold on to the
AnyCancellable objects are typically not around for the entire lifetime of your application.
Regardless, it’s good to be aware of this and I would recommend that you keep an eye out
for any potential memory problems related to persisted AnyCancellable instances even
though in my experience you shouldn’t run into problems with them.
In Summary
In this chapter, you learned a lot more about Combine’s publishers, subscribing to them and
also about the AnyCancellable that is created when you subscribe to a publisher. With
this new knowledge that you have about Combine’s sink and assign methods, you should
be able to subscribe to any publisher that is handed to you!
One of the things that I find so satisfying about Combine is that its foundation is so simple.
Donny Wals 28
Practical Combine
Once you understand the principles that I have explained in this chapter, you already have a lot
of knowledge about the set of rules that Combine operates in. Publishers send values over time
to their subscribers. They can only complete (or error) once. When you subscribe to a publisher
you can react to incoming values and the completion event using sink, or you can assign
values directly to a key path. Every subscription can be wrapped in an AnyCancellable
object that tears down its subscriber when it’s deallocated. There are some nuances to the
summary I have just provided but don’t sweat those for now. You will discover and learn about
the details as you move forward in this book.
So far, I have mentioned transformations and operators a few times but I haven’t taken the
time to explain them to you in-depth. The reason for that is simple; it’s what the next chapter
is about!
Donny Wals 29
Practical Combine
Transforming publishers
So far, you have learned that in Combine, all values are pushed to subscribers by publishers.
In some cases, the values are exactly what you need, but in most cases, the value that you
receive from a publisher is not quite what you need. Of course, you can manipulate a received
value in your sink, and that might work well but it can also get messy real quick.
Luckily, Combine comes with the ability to use operations like map to transform incoming
values into something else before delivering them to a subscriber. And their power doesn’t
stop there, we can even catch and handle errors, or limit the number of items we want to
receive from a publisher. In this chapter, you will learn the following about transformations:
You won’t learn everything about all the different operators that Combine has to offer. In-
stead, I will explain how these transforming operators work in Combine so you have a solid
understanding of them when we take a better look at them in some examples. By the end of
this chapter, you should be able to look at an operator and have a rough understanding of
what it does.
[1, 2, 3].publisher
.sink(receiveValue: { int in
Donny Wals 30
Practical Combine
In this code, we have a publisher that publishes integer values. In the sink, these integer
values are converted to strings so the received integer can be displayed on a label.
While there is nothing inherently wrong with this code, it’s common to avoid any processing
or manipulation in subscribers if we can. Especially if the transformations are complex. In this
case, we’re not doing much of a complex transformation, but it’s nice to improve this code
either way.
[1, 2, 3]
.publisher
.map({ int in
return "Current value: \(int)"
})
.sink(receiveValue: { string in
myLabel.text = string
})
If you’ve worked with collections before in Swift, map probably looks familiar to you. Calling
map on an array works much like calling map on a publisher. The only difference is that a
publisher delivers its values over time, or asynchronously while an array can deliver them all
at once. The output for calling map on an array is a new array. The output for calling map on a
publisher is, in fact, a new publisher. Let’s look at a marble diagram:
Donny Wals 31
Practical Combine
In Combine, marble diagrams are a common way to visualize how publishers change over
time. They are typically read top to bottom and left to right, so in this case, we’re looking at
a publisher that publishes values. Underneath this publisher, I wrote map to indicate that a
map operation is applied to this publisher. The resulting publisher is shown underneath the
map. It publishes the same number of values, except they have a different color to indicate
that the value was transformed.
Going back to the code, you can inspect the type of the publisher that is returned by map. The
returned type is the following:
Publishers.Sequence<[String], Never>
Donny Wals 32
Practical Combine
// URLSession.DataTaskPublisher
let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for:
,→ someURL)
// Publishers.Map<URLSession.DataTaskPublisher, Data>
let mappedPublisher = dataTaskPublisher
.map({ response in
return response.data
})
Notice that instead of having a single publisher, we are now looking at the initial data task
publisher, which is wrapped in Publishers.Map. This is vastly different from what map
did for the first publisher, so what’s going on here?
Tip: If you want to learn more about networking in Combine, it’s explained in-depth with
many examples in Chapter 6 - Using Combine for networking.
The explanation is quite simple. Combine comes with a ton of built-in operators that are
defined on the Publisher protocol. You can quickly find these operators in Apple’s docu-
mentation. If you read this page, you’ll find that towards the end of the overview it says the
following:
This is clearly what we saw when mapping the data task publisher. But it’s not what hap-
pened when I showed you how to map over a sequence publisher. The reason it’s so different
is that map has a default implementation that’s implemented as a protocol extension on
the Publisher protocol. This means that specific publishers are free to provide overrides,
which is exactly what Apple has done for Publishers.Sequence. You can verify this by
looking at the documentation for Publishers.Sequence.map. Instead of returning an
instance of Publishers.Map, this specific implementation of map returns a Publish-
ers.Sequence<[T], Failure> where T is the type of the element we’re mapping to.
Combine has many built-in publishers and you can find them all in Apple’s documenta-
tion for Publishers. There are several interesting publishers like Publishers.Scan,
Donny Wals 33
Practical Combine
Donny Wals 34
Practical Combine
Don’t worry if you didn’t quite get that, you’ve just started learning Combine and the chain of
operations above is quite sophisticated.
If you take another look at the code and consider how you would implement this without using
Combine, you’ll soon realize how much more complex the code would be. By chaining built-
in operators together you can achieve extremely complicated results, simply by taking the
output of a publisher, doing something with it, and outputting a new value. This is functional
programming at its best.
In this example, every element in the initial array of strings is converted to an Int. This
particular initializer for Int takes a String and returns either a valid Int or nil if the
String couldn’t be converted to an Int. A normal map would preserve the nil values
resulting in an array of Int?. Because we used compactMap, all nil results are dropped
and we get an array of Int instead.
Donny Wals 35
Practical Combine
It’s possible to use compactMap on publishers in Combine. This works similar to how map
works, and it applies the same rules that an imperative compactMap is built on. Let’s look at
another example:
The sink in this code will only receive the values 2, 4 and 5. Because we transform the
publisher’s output using compactMap, all nil values are dropped as expected. If you’d
use a regular map instead of compactMap to transform the output of this publisher, you
would end up with nil, 2, nil, 4 and lastly 5 being passed to the sink’s receiveValue
closure.
Using compactMap can save you some nil checks and guard statements if you want to
make sure that you don’t receive any nil values in your subscriber. Keep in mind though that
a nil value is dropped entirely. If you want to convert nil values to a default value, you can
use a regular map, and apply the replaceNil operator on the resulting publisher:
This has the benefit of filtering out nil values and replacing them with a non-nil value,
without completely dropping any values. The type of the object that you receive in the sink
is still Int? because replaceNil doesn’t change the output type of the publisher it’s
applied to, so for good measure, you could automatically unwrap every value by applying
compactMap to the output of replaceNil, but of course that’s entirely up to you:
Donny Wals 36
Practical Combine
If you need to decide whether you should use replaceNil or compactMap, the decision
should depend on two important factors:
• Is it okay to drop nil values completely? You should probably use compactMap.
• Can you replace nil with a sensible and valid default? Then replaceNil is likely a
good decision.
Having options like these available is what makes Combine into the powerhouse that it is. And
interestingly, there are often multiple ways to achieve the same result. For example, in the
earlier example, we could have used a map that returned Int($0) ?? 0 to ensure that we’d
never return nil. This would eliminate the need for both compactMap and replaceNil.
Each way of achieving a task has a different impact on the readability and performance of your
code, and it’s always important to consider multiple options and if you think performance
might be a factor, make sure to measure the impact that each possible choice has on your
app.
Luckily, we can find a somewhat better explanation in the Discussion section of the documen-
tation:
Donny Wals 37
Practical Combine
Use this method to receive a single-level collection when your transformation produces
a sequence or collection for each element.
And to top it off, Apple provides an easy to follow example that I’ve decided copy and paste:
In this example code, an array of numbers is transformed with map and flatMap respectively.
The transformation that is applied to each element turns the array of Int into an array of
[Int], which means it’s an array that contains other arrays. In the regular map, we get
back exactly that, an array of arrays. The flatMap example returns an array of Int. It has
“flattened” the nested arrays to make sure we’d get back an array with one removed level of
nesting. Using flatMap on an array is equivalent to using map and then calling joined()
on the resulting collection. You can try that out for yourself if you’d like.
So far we’ve been able to think of publishers in Combine as almost analogous to Swift’s
collections. For flatMap, this same analogy holds. If we want to apply an operator to the
output of a publisher that would transform that output into a new publisher, we’d have a
publisher that publishes other publishers. Let’s look at an example:
Donny Wals 38
Practical Combine
}, receiveValue: { result in
print(result)
})
This example uses a sequence publisher to publish several strings that point to pages on
my website. Each string is used to create a URL, and this URL is then used to create a new
DataTaskPublisher. The values that end up in the sink are not the results of the data
tasks. Instead, the publishers themselves are delivered to the sink. This is isn’t particularly
useful, and we can use flatMap to change this:
The code above doesn’t compile on iOS 13 but works fine for iOS 14. The sequence publisher’s
error type is Never, and the DataTaskPublisher has URLError as its error type. When
you use flatMap in Combine, the error type of the new publisher must match that of the source
publisher (unless you’re on iOS 14 and the source publisher’s Failure type is Never).
For iOS 13 we need to make sure that the publisher that we flatMap over has the same failure
type as the publisher that we create in the flatMap. So that means that we either need to
make sure that all failures from the data task that’s created in the flatMap are replaced with a
default value to make its failure type Never, or we need to change the sequence publisher’s
failure type to URLError.
Since we shouldn’t hide any URLErrors that are emitted by the data tasks created in the
flatMap above, we need to change the sequence publisher’s error type to match URLError.
Donny Wals 39
Practical Combine
We can do this with the setFailureType operator. This operator creates a new publisher
with an unchanged Output, but it changes the Failure to the error type you supply. You
would insert this operator before the flatMap operator and call it as follows for the example
above: .setFailureType(to: URLError.self).
This code compiles, but if you’d run it in a playground, nothing happens. That’s because
this code runs asynchronously which means that the AnyCancellable that we get from
the sink method is deallocated as soon as the current execution scope is exited, and the
subscription stream is torn down like I explained in the previous chapter. To fix this, you
need to hold on to the AnyCancellable like this:
If you run this code in a playground, you’ll find that the sink now receives the result of each
data task. You’ll also find that receiveCompletion isn’t called until the last data task has
finished. Neat, right? We used flatMap to transform a publisher that publishes string values
into a publisher that publishes data task publishers, and we flattened this hierarchy with
flatMap which made the intermediate data task publishers invisible to the sink! Refer to
the following marble diagram to see what this process looks like when visualized.
Donny Wals 40
Practical Combine
Donny Wals 41
Practical Combine
In this case, we wanted to get all of the results from the data tasks. But what if we’re in a
situation where that isn’t the case?
[1, 2, 3].publisher
.print()
.flatMap({ int in
return Array(repeating: int, count: 2).publisher
})
.sink(receiveValue: { value in
print("got: \(value)")
})
The following code should look familiar but there’s a new operator; print. With Combine’s
print operator, you can take a look at what’s happening upstream from that print operator.
Donny Wals 42
Practical Combine
So in this case, we get to take a peek at what the sequence publisher does. This code would
produce the following output:
These two lines don’t look like much but they contain a ton of information. First, it tells us
that the publisher receives a subscription at some point and that it then receives an unlimited
request. What this means is that the publisher is asked to produce as many items as it wants,
no limits. The publisher works in service of the subscriber so it immediately fulfills this
request and it begins sending values. But what happens if we replace the flatMap with
flatMap(maxPublishers:)? Let’s find out:
[1, 2, 3].publisher
.print()
.flatMap(maxPublishers: .max(1), { int in
return Array(repeating: int, count: 2).publisher
})
.sink(receiveValue: { value in
Donny Wals 43
Practical Combine
print("got: \(value)")
})
The code hasn’t changed much. The only difference is that the flatMap is passed a max-
Publishers value of .max(1). We can supply any Int value for the maximum number
of publishers. Alternative options are to pass .unlimited (the default), or .none which
would mean that we never receive any values at all. When you examine the output of this
code, you’ll find the following:
Notice how instead of request unlimited, the output now shows request max:
(1). This means that the flatMap has told the upstream publisher that it only wants to
receive a single value. Once the value is received, flatMap will wait for the created publisher
to complete before requesting a new value, again with max: (1). It continues to do this
until the upstream publisher completes and sends a completion event. I will come back to
Combine’s print operator and other debugging techniques in Chapter 10 - Debugging your
Combine code.
This example shows you backpressure management at its finest. It allows flatMap to manage
the number of publishers that it produces. The upstream publisher can choose to either buffer
Donny Wals 44
Practical Combine
events while flatMap isn’t ready to receive them, or the publisher can decide to drop them.
That’s an implementation detail of the publisher you’re working with.
Throughout this book I will come back to backpressure management, flatMap and limiting
the number of active publishers, but for now I think we should move on and explore some
other operators.
Don’t worry if you’re slightly confused about flatMap and backpressure management at this
point. The flatMap operator is probably one of the more powerful and complex ones I have
seen, especially because it integrates with backpressure so tightly. It will all become clear as
we go along on our journey to learn Combine.
[1, 2, 3].publisher
.tryMap({ int in
guard int < 3 else {
throw MyError.outOfBounds
}
Donny Wals 45
Practical Combine
return int * 2
})
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { val in
print(val)
})
This example uses tryMap to map over a publisher that emits integer values. If we encounter
an integer that isn’t smaller than three, that’s considered an error and an error is thrown. Keep
in mind that publishers can only complete or emit an error once. This means that after an
error is thrown, the publisher can’t emit new values. This is important to consider when you
throw an error from an operator. Once the error is thrown, there is no going back.
The example I just showed you might not be the best use case of a tryMap. Its purpose wasn’t
to show you an elaborate use case of tryMap. Instead, I wanted to show you how you can
use it, and give you something to play with. I’m sure that if you encounter a situation where
throwing an error from an operator makes sense, you’ll know to look for the try prefixed
operator you want to use.
I do think that there is one very important detail to point out. When I showed you how to use
flatMap in the previous section, you learned that on iOS 13 you have to use the setFail-
ureType operator to change the sequence publisher’s Failure from Never to URLError.
In the example I just showed you, we were able to change the error from Never to MyError
without doing so explicitly. Even on iOS 13. The reason for this is that when an operator directly
influences the error type like this example does, Combine can safely infer and change the
error type that is exposed downstream to other operators or subscribers.
Donny Wals 46
Practical Combine
your code more readable, and easier to reason about. Let’s take another look at some code
I’ve shown you earlier in this chapter:
This code applies two operators to a publisher that emits strings. One to set the publisher’s
failure type to URLError, and one to convert the string to a data task publisher. This code
isn’t necessarily hard to read, but the two operators we apply in this example are coupled
pretty tightly. And you’re also writing code that’s only needed on iOS 13 where it’s not strictly
needed. While it’s not a performance issue or anything, it would be nice to clean up the code a
bit and combine the setFailureType and flatMap operators into a single operator. This
makes the code shorter, easier to read and easier to maintain because you can hide the iOS 13
specific operator from the rest of your code. Let’s see how this can be done:
Donny Wals 47
Practical Combine
.eraseToAnyPublisher()
} else {
return self
.setFailureType(to: URLError.self)
.flatMap({ path -> URLSession.DataTaskPublisher in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
.eraseToAnyPublisher()
}
}
}
Tip: I’m using Swift’s #available to check if iOS 14 or newer is available. If it is, I know
that I don’t need the .setFailureType(to:) operator. If iOS 14 or newer is not
available this means that we need to apply setFailureType(to:).
All operators in Combine are defined as an extension on Publisher. These extensions can
be constrained to ensure that a publisher has a certain output or failure type. In this case,
I constrained the extension to publishers that have String as their output, and Never
as their error. This matches the output and error of the string publisher from the code
we’re refactoring. At its core, this custom operator applies the same two operators that
were applied in the original code. Note that a third operator is applied; eraseToAnyPub-
lisher(). This operator removes all type information from the publisher and wraps it in an
AnyPublisher. This is a good thing because the publisher that we end up with after apply-
ing flatMap is Publishers.FlatMap<P, Publishers.SetFailureType<Self,
URLError>>. This isn’t the most readable and useful return type. It’s also an implemen-
tation detail of our operator. By erasing this implementation detail, we can return Any-
Publisher<URLSession.DataTaskPublisher.Output, URLError>. This tells
users of our custom operator everything they need to know. This custom operator can be
used as follows:
Donny Wals 48
Practical Combine
The code is a little bit shorter, but it’s also easier to read. Readers of this code will understand
that every string that is published by the publisher is converted to a data task.
While you can shorten code and make it more readable with custom operators, doing so comes
with some cost. People that are familiar with Combine but are new to your codebase won’t be
familiar with your custom operators. This might introduce unneeded complexity and friction
for developers on your team.
On the other hand, a couple of well-defined and well-documented custom operators can
be a huge asset for your codebase. Whenever you’re about to introduce a custom operator,
consider the implications and ask yourself whether the pattern you’re abstracting is common
enough to warrant a custom operator.
In Summary
In this chapter I wanted to show what operators in Combine are, and how you can use them. I
started by giving you a high-level overview, and we went more in-depth later. You learned how
you can transform a publisher’s output with operators like map, compactMap and flatMap.
You saw how these operators work, what their similarities are and what their differences
are.
Along the way, I introduced you to several other operators like setFailureType, re-
placeNil and replaceError. You saw that all operators in Combine work in a similar
manner. They take a publisher’s output and/or error and return a new publisher with a modi-
fied output and/or error.
You also learned that operators don’t typically throw errors and that most Combine operators
come with a separate version that has a try prefix that allows you to throw errors when
Donny Wals 49
Practical Combine
appropriate. When using these operators, keep in mind that a publisher chain can only emit
an error once. When a publisher emits an error, the stream is considered completed and it
can’t emit any new values.
Lastly, I showed you how you can define custom operators by extending Publisher and con-
straining this extension as needed. It’s not very common to need or define custom operators,
but it’s good to know how to do it because a well-placed custom operator can dramatically
improve a codebase.
This chapter wraps up the introductory, theoretical part of this book. You now know about all
of Combine’s basic building blocks. In Chapter 1 - Introducing Combine you learned what
Combine and Functional Reactive Programming are. In Chapter 2 - Exploring publishers
and subscribers I explained what publishers are, how they work, and how you can subscribe
to them using sink and assign(to:on:). Now that you learned about operators, we’re
ready to move on to the fun part, seeing how Combine fits in your existing projects, and how
you can gradually introduce Combine into your toolbox!
Donny Wals 50
Practical Combine
Before we get started I should mention that I will use a simple and loose version of MVVM to
demonstrate most principles of using Combine to update your user interface. This doesn’t
mean that it’s the only way to effectively use Combine. It’s just a way to show you how you
can separate code in lightweight objects, and everything you’ll learn can be adapted to any
other architecture you might want to use with relative ease.
Donny Wals 51
Practical Combine
Each of these techniques serves a different purpose in Combine, and I will go over these
techniques one by one. Let’s start with the PassthroughSubject.
Donny Wals 52
Practical Combine
existing, imperative code that doesn’t hold a state. This makes a PassthroughSubject a
good fit for publishing values that represent ephemeral data, like events.
For example, in a game, you might use a PassthroughSubject to communicate that a
user has collected an item, or that they completed a level. The PassthroughSubject’s
subscribers can then use this information to update the state of the program or to handle the
occurred event.
If you’ve built iOS applications before, you might be familiar with NotificationCenter.
The NotficationCenter in iOS is an object that broadcasts events throughout your ap-
plication to all interested objects. These events range from application lifecycle events to
events that inform you when the keyboard is about to appear or disappear. Apple has created
a built-in publisher for NotificationCenter that can be used as follows:
publisher
.sink(receiveValue: { notification in
print(notification)
}).store(in: &cancellables)
notificationCenter.post(Notification(name: notificationName))
If you run this code in a Playground, you’ll find that as soon as you post a notification to
the notification center, you receive this value in the sink. The type of publisher that is
created by calling publisher(_:) on NotificationCenter is a NotificationCen-
ter.Publisher. However, it fits my description of a PassthroughSubject really well
because it displays the same behavior as I describe for PassthroughSubject. Let’s re-
implement NotificationCenter.Publisher using a PassthroughSubject to see
just how similar they are.
Donny Wals 53
Practical Combine
notificationSubject
.sink(receiveValue: { notification in
print(notification)
}).store(in: &cancellables)
notificationCenter.post(Notification(name: notificationName))
Donny Wals 54
Practical Combine
class Car {
var onBatteryChargeChanged: ((Double) -> Void)?
var kwhInBattery = 50.0 {
didSet {
onBatteryChargeChanged?(kwhInBattery)
}
}
kwhInBattery -= kwhNeeded
}
}
The preceding code is relatively simple, and the part that I want you to focus on is the didSet.
Whenever the car’s kwhInBattery is updated, an optional closure is called. This closure
can be set by the owner of this car model as follows:
Donny Wals 55
Practical Combine
car.onBatteryChargeChanged = { newCharge in
someLabel.text = "The car now has \(newCharge)kwh in its battery"
}
If you were to implement a pattern like this in your app, your someLabel would automatically
be updated every time kwhInBattery changes. We can implement a very similar pattern
using Combine’s CurrentValueSubject, and the code would be a bit cleaner too. Let’s
see how:
class Car {
var kwhInBattery = CurrentValueSubject<Double, Never>(50.0)
let kwhPerKilometer = 0.14
kwhInBattery.value -= kwhNeeded
}
}
In this new version of Car, kwhInBattery is no longer Double. Instead, it’s a Current-
ValueSubject with an output of Double and a failure type of Never. Notice that in the
Donny Wals 56
Practical Combine
drive(kilometers:) method, I had to change the code a little bit. Instead of access-
ing kwhInBattery directly, its current value is accessed by reading the subject’s value
property. A CurrentValueSubject holds state which means that we can read its current
value through the value property. This is something we can’t do with a Passthrough-
Subject.
Notice that there is no explicit call to send(_:) in this example. When you change a Cur-
rentValueSubject’s value property, it automatically sends this new value downstream
to its subscribers. We don’t need to do this ourselves. Also, note that we don’t read the
initial value of kwhInBattery to configure the label. That’s not a mistake. A Current-
ValueSubject immediately sends its current value to any new subscribers. And because
a CurrentValueSubject must always be initialized with an initial value, it will always
have an initial value to send to its subscribers. So in the example I just showed you, your label
should display the battery’s initial charge which is 50. If you call drive after subscribing to
kwhInBattery, you will see that the label updates immediately to show the battery’s new
charge:
Donny Wals 57
Practical Combine
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
kwhInBattery -= kwhNeeded
}
}
When you use @Published, you can access and modify the value of kwhInBattery di-
rectly. This is really convenient and makes the code look nice and clean. However, because
kwhInBattery now refers to the underlying Double value, we can’t subscribe to it di-
rectly.
Donny Wals 58
Practical Combine
To subscribe to a @Published property’s changes, you need to use a $ prefix on the prop-
erty name. This is a special convention for property wrappers that allows you to access the
wrapper itself, also known as a projected value rather than the value that is wrapped by the
property. In this case, the wrapper’s projected value is a publisher, so we can subscribe to the
$kwhInBattery property.
Like I mentioned earlier, properties that are annotated with the @Published property wrap-
per mostly exhibit the same behaviors as CurrentValueSubject from the perspective
of subscribers. The main difference is that a @Published value will update its underlying
value after emitting the value to its subscribers, the CurrentValueSubject will update
its value before emitting the value to its subscribers. The following example does a nice job of
demonstrating this difference:
class Counter {
@Published var publishedValue = 1
var subjectValue = CurrentValueSubject<Int, Never>(1)
}
let c = Counter()
c.$publishedValue.sink(receiveValue: { int in
print("published", int == c.publishedValue)
})
c.subjectValue.sink(receiveValue: { int in
print("subject", int == c.subjectValue.value)
})
c.publishedValue = 2
c.subjectValue.value = 2
published, true
subject, true
published, false
subject, true
Donny Wals 59
Practical Combine
There is also a limitation when using @Published though. You can only use this property
wrapper on properties of classes while CurrentValueSubject is available for both structs
and classes. In addition to this limitation, it’s also not possible to call send on a @Published
property because it’s not a Subject. In practice, this means that you can’t emit a completion
event for @Published properties like you can for a Subject. Assigning a new value to the
@Published property automatically emits this new value to subscribers which is equivalent
to calling send(_:) with a new value.
Donny Wals 60
Practical Combine
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
kwhInBattery -= kwhNeeded
}
}
The text property of someLabel in this example is always set to a new string that reflects
the car’s current battery status. Let’s expand this example a little bit and introduce a view
controller and a view model:
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
}
struct CarViewModel {
var car: Car
Donny Wals 61
Practical Combine
car.kwhInBattery -= kwhNeeded
}
}
class CarStatusViewController {
let label = UILabel()
let button = UIButton()
var viewModel: CarViewModel
var cancellables = Set<AnyCancellable>()
init(viewModel: CarViewModel) {
self.viewModel = viewModel
}
func setupLabel() {
// label setup will go here
}
func buttonTapped() {
viewModel.drive(kilometers: 10)
}
}
I have left out some repetitive and irrelevant code here, like the initializers for CarSta-
tusViewController and the code needed to add a tap handler to the button in the view
controller. Either way, I’m sure the code above shouldn’t hold too many surprises for you.
Regardless of whether you’re familiar with MVVM, and regardless of whether you think this is
a good example of MVVM. The main reason I’ve used a view model in this example is to add
Donny Wals 62
Practical Combine
a layer between the Car which holds the @Published property, and the label that will
ultimately show the amount of battery power that’s left in the car’s battery.
Because we ultimately want to have the view model in this code prepare a string for the label,
the view model will need to both subscribe to the car’s @Published kwhInBattery, and
expose a new publisher to send formatted strings to the label. We can do this by adding the
following property to the view model:
This code defines a lazy property. This means that it will not be initialized until the first
time it is read. Notice that the property’s value is wrapped in { ... }(). This means
that we want to initialize this property with the result of the code between the { and }.
The reason batterySubject is lazy is due to the fact that we need to access the car
property to create the batterySubject. If we don’t make batterySubject lazy the
code wouldn’t compile because self would not be initialized when batterySubject
would be initialized.
The batterySubject is an AnyPublisher<String?, Never>. We create this pub-
lisher by grabbing the car’s $kwhInBattery publisher, mapping over it to convert the
current charge into a string, and erasing the result to AnyPublisher to ensure we have a
nice and clean type for this publisher. The reason the publisher’s value type is String? and
not String will become clear in a moment.
Now that we have a property to subscribe to in the view model, it’s time to subscribe to
the view model’s publisher in the view controller. To do this, we need to update the view
controller’s empty setupLabel() method from the earlier code snippet:
func setupLabel() {
viewModel.batterySubject
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
Donny Wals 63
Practical Combine
Notice how clean this code is. By using a new subscriber that I haven’t talked about before,
we don’t need to provide any closures or do any processing on the view model’s output.
Instead, the values that are published by batterySubject are assigned to the label’s
text property immediately. To do this, the label’s text and the batterySubject output
must match. Since text is a String?, we needed to use String? as the output type for
batterySubject. I hope that this is a limitation in Combine that could be lifted in future
releases, but as of now there isn’t much else we can do.
The assign(to:on:) operator in Combine is similar to sink. It creates a new Sub-
scriber, and it returns an AnyCancellable that we must retain to keep the subscription
alive.
When you use assign(to:on), you don’t provide any closures or do any extra processing
at all. Instead, all published values are assigned to the keypath that you pass as to:, on the
object that you pass as on:. Note that currently there is an issue with assigning to keypaths
on self. Doing this will cause a retain cycle to occur. You can read more about this issue, and
whether it is resolved, on the Swift forums.
Now that you’ve seen how you can take a published value from a model, modify it in a view
model and assign it to a label’s text using assign(to:on:), you should be starting to see
how Combine can help you move your code from being the old, imperative way to the new,
reactive way. There are many ways to achieve your goals, and what I’ve demonstrated in this
section is far from the only way to implement reactive programming and MVVM, but it’s a way
that works for me, and it’s simple enough to explain and follow without letting it get in the
way of our main goal, which is to learn Combine.
Donny Wals 64
Practical Combine
assume you have a fair knowledge of collection views, and that you know how to set up a
collection view layout. Possible with a compositional layout, which I’ve covered in one of my
blog posts. I will also be using iOS 13’s diffable data source, which you can learn more about
on my blog. I will explain the most important bits as I touch them in this post, but since this is
a book about Combine, I will focus on the Combine part of building collection views.
This section is split up in two parts:
class DataProvider {
let dataSubject = CurrentValueSubject<[CardModel], Never>([])
func fetch() {
let cards = (0..<20).map { i in
Donny Wals 65
Practical Combine
dataSubject.value = cards
}
}
This code is relatively simple, and shouldn’t contain anything you don’t already know. The
principle here is that we have a property called dataSubject. This is a CurrentValue-
Subject which allows other objects to subscribe to this subject and obtain the currently
fetched data. In this example, I assumed that we’re going to build something where every time
we fetch data, the new data replaces the existing data. By decoupling the fetch() logic and
the dataSubject like this, we can have multiple objects that subscribe to dataSubject,
and when any object calls fetch(), all objects that subscribe to dataSubject automati-
cally receive the updated data. An alternative design could have been the following:
class DataProvider {
func fetch() -> AnyPublisher<[CardModel], Never> {
let cards = (0..<20).map { i in
CardModel(title: "Title \(i)", subTitle: "Subtitle \(i)",
,→ imageName: "image_\(i)")
}
return Just(cards).eraseToAnyPublisher()
}
}
In this alternative example, the fetch() method would return a publisher. Because this
example executes synchronously and doesn’t make any network calls, this is pretty simple. I
generate data, and I return Just(cards).eraseToAnyPublisher().
Both approaches of implementing DataProvider are perfectly fine, and the right choice for
your app depends on your use case. If you only want to fetch data once, without appending
new data to previous data sets or updating other objects that might be interested in the
Donny Wals 66
Practical Combine
same new data, then the second approach is perfect for you. If you’re building something
where data should be broadcast to one or more interested subscribers, and where the fetched
data should remain available at any time, then a CurrentValueSubject is for you. A
CurrentValueSubject is also very convenient if you’re building a collection view that
needs to implement infinite scrolling. Before I show you how to update a collection view, I want
to quickly show you an example of what a simple, naive implementation of DataProvider
looks like if it supported an infinitely scrolling collection view:
class DataProvider {
let dataSubject = CurrentValueSubject<[CardModel], Never>([])
var currentPage = 0
var cancellables = Set<AnyCancellable>()
func fetchNextPage() {
let url = URL(string:
,→ "https://myserver.com/page/\(currentPage)")!
currentPage += 1
URLSession.shared.dataTaskPublisher(for: url)
.sink(receiveCompletion: { _ in
// handle completion
}, receiveValue: { [weak self] value in
guard let self = self else { return }
There is a better way to write this code that I’ll cover in Chapter 12 - Driving publishers
and flows with subjects. For now, it’s important that you understand what this code does,
Donny Wals 67
Practical Combine
and why it works. Every time fetchNextPage() is called, we increment the current
page counter, and a URL is created. This URL points to the page of data that we want to
fetch from a server. Most web APIs that provide pagination functionality will require you to
provide a page number so it knows which set of data to return. In this code, a URLSes-
sion.DataTaskPublisher is used to fetch the required data, which is then decoded
into an array of models and this array of models is then appended to the existing data in the
dataSubject. Every time fetchNextPage() is called and it managed to retrieve data
successfully, all subscribers of dataSubject automatically receive the latest state of the
complete data set. Cool stuff, right!
In the following code, I’m going to assume that we’re using the very first DataProvider
implementation I’ve shown you. It’s the one that generates data and then assigns the result
to a CurrentValueSubject. I’m going to assume that you have a diffable data source set
up already. If you’re not sure how to do this, refer to the blog post I mentioned earlier in this
section.
Let’s dive right in, and look at some code:
dataProvider.dataSubject
.sink(receiveValue: self.applySnapshot)
.store(in: &cancellables)
}
snapshot.appendItems(models)
datasource.apply(snapshot)
}
If you already have a datasource property set up for your collection view, this is enough
code to fetch data and update your collection view using a diffable data source.
Donny Wals 68
Practical Combine
Donny Wals 69
Practical Combine
without Combine just fine. I’m going to point you to yet another blog post that I wrote where
I explain how you can asynchronously load images for table views and collection views.
In this section, I will show you a mechanism where Combine is used to retrieve and set images
on a collection view cell. We’ll start with an update to the DataProvider to have it create
URLSession.DataTaskPublisher instances that can be used to retrieve images:
This method is pretty straightforward. It creates a data task publisher and tries to transform a
successful response into a UIImage which will be displayed on a collection view cell’s image
view.
To use this method effectively, we’re going to need to make an important decision. Where
should fetchImage(named:) be called from, who should subscribe to it, and where
should the returned Cancellable be stored.
Deciding where the Cancellable should be stored is somewhat of a no-brainer. If we don’t
store the AnyCancellable that is returned by sink or assign anywhere, the subscription
stream is torn down and we never receive a result. This kind of cancellation needs to be done
every time a collection view cell is reused because a cell should only be subscribed to the
publisher that is fetching the image for its current model. What this means is that if a collection
view cell has a variable that can hold on to a Cancellable, we can set that cancellable to
the image publisher for the current model, which will automatically deallocate and tear down
any previous subscriptions. Let’s look at a collection view cell with such a variable.
Donny Wals 70
Practical Combine
This example cell has an image and a cancellable property. Note that I used Can-
cellable and not AnyCancellable as its type. I like to write my code as flexible as
reasonably possible, and since AnyCancellable conforms to the Cancellable proto-
col, I figured this would be a good choice.
Now let’s see how we’d tie the DataFetcher to the CardCell and update its im-
ageView:
cell.cancellable = self.dataProvider.fetchImage(named:
,→ item.imageName)
.sink(receiveCompletion: { completion in
// handle errors if needed
}, receiveValue: { image in
DispatchQueue.main.async {
cell.imageView.image = image
}
})
return cell
}
By calling fetchImage(named:) in the diffable data source’s cell provider and subscribing
to it right there, we can assign the returned AnyCancellable to the cell’s cancellable
Donny Wals 71
Practical Combine
property. We can also assign the fetched image to the imageView here. And because the
subscription stream is torn down as soon as a new AnyCancellable is assigned to the cell’s
cancellable property, we don’t have to worry about receiving old images, or receiving
images out of order. All we need to do is make sure to set the cell’s imageView.image to
nil in the cell’s prepareForReuse method to prevent old images from showing up while
the new image is loading.
Note that I’m using DispatchQueue.main.async here to set the imageView’s image
on the main thread to prevent crashes. In Chapter 8 - Understanding Combine’s Schedulers
you will learn about the receive(on:) operator that will make dispatching to the main
queue obsolete because it allows you to receive values from a publisher on the main thread.
You can give it a try by placing .receive(on: DispatchQueue.main) before the sink
in the code snippet you just saw.
This technique of subscribing your cell to publishers in the cell provider is a common way
to make your collection or table view cells reactive. The example I’ve shown you is one of
the cases where your cells actually have to be dynamic due to the asynchronous loading of
images. In other cases, ask yourself if you’re not overcomplicating things by using Combine to
drive your cells. Chances are that you don’t need to make it this hard for yourself, but if you
do, now you know one way of driving your cells with a Combine publisher.
Donny Wals 72
Practical Combine
communicates the result of this work back through the @Published property.
In the previous section, I showed you a very simple and basic DataFetcher that supports
pagination. Let’s take another quick look at that code:
class DataProvider {
let dataSubject = CurrentValueSubject<[CardModel], Never>([])
var currentPage = 0
var cancellables = Set<AnyCancellable>()
func fetchNextPage() {
let url = URL(string:
,→ "https://myserver.com/page/\(currentPage)")!
currentPage += 1
URLSession.shared.dataTaskPublisher(for: url)
.sink(receiveCompletion: { _ in
// handle completion
}, receiveValue: { [weak self] value in
guard let self = self else { return }
Notice how I’m using a CurrentValueSubject to publish newly loaded data. To do this, I
have to subscribe to the publisher created in fetchNextPage() and update dataSub-
ject in the receiveValue closure.
I’d rather not have to subscribe to anything in fetchNextPage() and connect my data task
to the dataSubject directly. One of the ways this can be achieved is with assign(to:)
Donny Wals 73
Practical Combine
class DataProvider {
@Published var fetchedModels = [CardModel]()
var currentPage = 0
func fetchNextPage() {
let url = URL(string:
,→ "https://myserver.com/page/\(currentPage)")!
currentPage += 1
URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ [weak self] value -> [CardModel] in
let jsonDecoder = JSONDecoder()
let models = try jsonDecoder.decode([CardModel].self, from:
,→ value.data)
let currentModels = self?.fetchedModels ?? []
Donny Wals 74
Practical Combine
causing it to emit its newly obtained value to its subscribers. Note that assign(to:) takes
its target as an inout parameter which is why we need to prefix it with &.
Note that assign(to:) can only be used on publishers that have Never as their Failure
type because a @Published property also has Never as its Failure type. In the code
above this is achieved by replacing all errors with a default value to ensure that we can’t end
up with an error after the replaceError operator.
Also note that we don’t have to hold on to an AnyCancellable after calling assign(to:).
The assign(to:) subscriber manages its own cancellable and doesn’t return one for us to
manage. You can think of the assign(to:) subscriber as somewhat of a glue subscriber.
Its lifecycle is tied to the @Published property that it assigns values to.
While I’m quite happy with not having to subscribe to anything to make this example work,
there is another benefit to the approach I took here and that’s related to SwiftUI.
In SwiftUI, you can’t connect your UI to a publisher directly using assign(to:on:) or
sink. Instead, everything in SwiftUI is handled through property wrappers that manage state,
like the @ObservedObject and @StateObject property wrappers for example. Both of
these property wrappers are used to wrap objects that conform to the ObservableObject
protocol.
The ObservableObject protocol can be applied to classes like the DataFetcher I just
showed you and it automatically adds an objectWillChangePublisher property to
conforming classes. This property observes all @Published properties contained in the
ObservableObject and emits a value through its objectWillChangePublisher
property if any @Published property changes. This allows SwiftUI to update the view
rendering so it reflects the ObservableObject’s current state.
Since you can’t subscribe to publishers directly in SwiftUI, I’m sure you can imagine that being
able to conveniently pipe any publisher into a @Published property using assign(to:)
is quite nice for apps that make heavy use of SwiftUI.
I digress. . .
I promised you that I would try to keep the UI framework related content to a minimum. I
just thought that this was a neat bit of background information to help you understand the
purpose of the assign(to:) subscriber.
Donny Wals 75
Practical Combine
class ThemeManager {
var shouldOverrideSystemSetting: Bool {
get { UserDefaults.standard.bool(forKey:
,→ "ThemeManager.shouldOverrideSystemSetting") }
set { UserDefaults.standard.set(newValue, forKey:
,→ "ThemeManager.shouldOverrideSystemSetting") }
}
Nothing fancy here, right? Just two properties that map to a key in the UserDefaults store.
To use the ThemeManager object, you would typically create an instance of the manager in
your SceneDelegate, and pass it along to all view controllers that might be interested in
using the theme manager to update the user’s preferences, or to update their UI according
to the theme manager’s settings. This technique is called dependency injection. Your view
Donny Wals 76
Practical Combine
controllers depend on the theme manager, and you inject the theme manager into your view
controllers directly through their initializers, or you can inject the theme manager by assigning
it to a property on a view controller.
Alternatively, you can make ThemeManager a singleton if that fits your workflow and code-
base better. Singletons are often considered an anti-pattern and I prefer dependency injection
as I described earlier, but either technique should work if you want to implement a theme
manager of your own.
The code in its current form isn’t very reactive. The theme manager lacks a Current-
ValueSubject that will tell us whether a view controller should be dark, light, or use the
system settings. This means that we need to update the CurrentValueSubject every
time shouldApplyDarkMode, or shouldOverrideSystemSetting changes. Let’s
see how that looks in an updated implementation:
class ThemeManager {
enum PreferredUserInterfaceStyle {
case dark, light, system
}
if shouldOverrideSystemSetting {
preferredStyle = shouldApplyDarkMode ? .dark : .light
}
return CurrentValueSubject<PreferredUserInterfaceStyle,
,→ Never>(preferredStyle)
}()
Donny Wals 77
Practical Combine
updateThemeSubject()
}
}
A lot is going on in this code. Because there are essentially three states for the user’s preferred
theme, I created an enum that can be used to represent the preferred theme in the current
value subject. The subject itself is a lazy variable because I want it to be initialized only when
somebody accesses it. In the initialization closure, I check what the user’s settings are, and
based on that the current value subject receives its initial value.
The properties from the previous version of ThemeManager are updated. When a new value
is assigned to either shouldOverrideSystemSetting, or shouldApplyDarkMode,
updateThemeSubject() is called, and themeSubject’s value is updated which, in
turn, will send the user’s new preference to all of its subscribers. A theme manager like this is
fairly simple, but also extremely powerful. Let’s see how you would use this theme manager
in a view controller:
Donny Wals 78
Practical Combine
themeManager.themeSubject.sink(receiveValue: { style in
switch style {
case .system:
overrideUserInterfaceStyle = .unspecified
case .dark:
overrideUserInterfaceStyle = .dark
case .light:
overrideUserInterfaceStyle = .light
}
}).store(in: &cancellables)
}
This code uses a themeManager that I assume was injected into a view controller. By sub-
scribing to the themeManager’s themeSubject, the view controller can update its over-
rideUserInterfaceStyle property to correspond with the supplied style preferences.
By setting overrideUserInterfaceStyle, the view controller will automatically know
whether this view controller should be light, dark, or whatever the system setting is. By in-
jecting the same instance of ThemeManager into all view controllers, you will have a very
powerful theming system in your hands.
I’ve mentioned dependency injection a few times now, and I realize you might not be familiar
with it. In its simplest form, you would pass a dependency like the theme manager to a view
controller’s initializer. This only works if you don’t use storyboards. The following code shows
how you would create and inject the theme manager into a view controller from the scene
delegate:
Donny Wals 79
Practical Combine
init(themeManager: ThemeManager) {
self.themeManager = themeManager
I hope this shows that dependency injection sounds a lot more complex than it really is. It’s
really just the idea of having an object pass dependencies down to the objects it creates
or manages. So if we’d go from MainController to a next view controller, it would be
MainController’s job to initialize that next view controller and pass the theme manager
to it.
Tip: Since iOS 13, it’s possible to achieve dependency injection with storyboards. I won’t
explain how in this book, but I have a post on my blog that explains how you can do this
exactly.
Donny Wals 80
Practical Combine
In Summary
In this chapter, you took some big leaps towards being able to use Combine in your apps. First,
you learned about CurrentValueSubject and PassthroughSubject. You learned
about their similarities, differences and use cases. You now know that both subjects allow
you to send values to subscribers at will, and you know that a current value subject has a
sense of state in the form of the last published value. You also briefly learned about @Pub-
lished, and how it’s similar to a CurrentValueSubject, except it’s not a Subject like
CurrentValueSubject is.
Next, you learned how you can assign values published by a publisher to a key path using the
assign(to:on:) method. This method allows you to easily update key paths on elements,
like labels, with new data as soon as it becomes available. This is a very nice and clean way to
integrate Combine in your user interface.
As a more practical exercise, you learned how you can integrate Combine in your collection
views to update its data source, and how you can integrate Combine with your collection view
cells to update a cell’s data dynamically. I demonstrated this by showing you how to load
images for your collection view cells with a data task publisher.
Lastly, I demonstrated how you can build a simple theming system that allows your users
to switch your app between light and dark mode independent of the system setting with a
relatively small amount of code. There is a way to clean that code up a little bit and make
it shorter by changing the type of value that’s published by the theme manager’s theme
subject. I’m going to leave that optimization up to you as an exercise. I have also used
CurrentValueSubject for all examples. Some of these examples would work really well
with @Published too. Can you figure out which ones? And do you think you can refactor
these examples to work with @Published? I’m sure you can! Give it a shot and see.
In the next chapter, we’re going to explore user interaction and you’ll learn how you can handle
user input in Combine.
Donny Wals 81
Practical Combine
By the end of this chapter, you should be able to build a moderately advanced app with
Combine that handles user input and responds to changing values in your app. Sounds good,
doesn’t it?
Donny Wals 82
Practical Combine
Let’s start with a relatively simple example. What if you wanted to build a slider that, when
changed, updates a property that is bound to a published property? Here’s what that might
look like without Combine:
updateLabel()
slider.addTarget(self, action: #selector(updateLabel), for:
,→ .valueChanged)
}
This approach looks fine upon first inspection. But there are a few gotchas here. First, when
you set slider.value directly, it doesn’t trigger a valueChanged action on the slider.
This means that if you want to update the slider’s value directly, you need to make sure that
you also update the label because the action for the slider doesn’t trigger when you assign
the value directly. An alternative approach would be to use an intermediate property with a
didSet attached to it:
Donny Wals 83
Practical Combine
This is much better, but the didSet isn’t triggered until the first time the slider’s value is
updated. So the label won’t have text until the user moves the slider for the first time. Of
course, it’s possible to update the label in viewDidLoad just once to make sure it has a value.
Instead of fixing and improving the UIKit-only solution, we’re going to solve this problem with
some Combine code instead:
Donny Wals 84
Practical Combine
$sliderValue
.map({ value in
"Slider is at \(value)"
})
.assign(to: \.text, on: label)
.store(in: &cancellables)
$sliderValue
.assign(to: \.value, on: slider)
.store(in: &cancellables)
There is more code involved in this solution than there was in the UIKit solution. However,
this code updates the UI perfectly whenever sliderValue changes and both the label and
slider are given their initial values directly through the published property.
Unfortunately, Combine doesn’t come with good built-in integration for UIKit. This means
that there is no obvious or built-in way for developers to bind sliderValue to the slider.
What I mean by this binding here is a two-way binding. In a two-way binding, the slider’s
current value is updated whenever sliderValue changes, and the opposite happens when
the slider is changed by the user.
Donny Wals 85
Practical Combine
Because Combine doesn’t natively have any publishers that work for slider changes, we need
to subscribe to the sliderValue and update it separately. On the other hand, SwiftUI has
excellent support for bindings like the one we’d like to achieve here. Let’s look at a SwiftUI
example of a view that has a label and a slider which are set up similarly to the UIKit code
you just saw, with the main difference being that we don’t need to explicitly update the
sliderValue:
In SwiftUI, properties that are marked with @State trigger UI updates when they change.
They can also be used to create bindings between a UI element, and your app’s state like
this example shows. I don’t know whether @State uses Combine internally, or whether it
uses some other mechanism to update the view when the value of the wrapped property
changes. What I do know, is that Apple seems to have decided that the kind of binding I just
demonstrated is important for SwiftUI and that it’s not as important in UIKit.
In Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions I will show
you how you can build a custom publisher that adds an extension to UIKit’s UIControl class
Donny Wals 86
Practical Combine
that will allow you to use Combine to respond to user events. In that chapter, I will also point
you to a set of custom publishers that were created by the community to help you subscribe
to changing UI components more conveniently.
For now, you must understand that you can use the good old addTarget(_:action:for:)
method in UIKit to catch the user’s input, and update a property that resembles a value that
should be used to represent the current state of the UI. By making the value you’re updating
@Published, you gain the ability to easily subscribe to the property that you’re changing,
and update your UI.
Donny Wals 87
Practical Combine
The code in this example looks very similar to the code I’ve presented in the previous section.
Whenever the text field’s value changes, a @Published property is updated, and the label
that I’ve added to this example is immediately updated with the new text. Instead of making
a network call, I will update the label that shows the text field’s current value using the same
technique that you’d normally use to debounce the input for network calls.
Combine comes with a built-in debounce() operator that we can apply to a publisher. This
will limit that publisher’s output, by ignoring values that are rapidly followed by another value.
Let’s look at a marble diagram that demonstrates this principle:
Donny Wals 88
Practical Combine
In this diagram, you can see that whenever a publisher emits several values in a short time-
frame, these values are never delivered to the subscriber. The subscriber will only receive
values that were not followed up by a new value within 300 milliseconds.
To integrate this in for the text field, all we need to change is how we subscribe to $search-
Query:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.assign(to: \.text, on: label)
.store(in: &cancellables)
The changes required to debounce the output of $searchQuery is pretty minimal, but the
effect is huge. Now, when a user is typing and changing their search query, the label isn’t
immediately updated. Instead, the user will have to stop typing for a moment, and the label
is updated. If you would replace .assign(to: \.text, on: label) with a network
call, you’re already saving yourself a lot of bandwidth!
Remember that I mentioned a requirement where a user had to type at least a couple of
Donny Wals 89
Practical Combine
characters before we’re interested in processing the search query? We can achieve this by
filtering the output of a publisher using the filter operator:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.filter({ ($0 ?? "").count > 2 })
.assign(to: \.text, on: label)
.store(in: &cancellables)
Filtering output that’s considered too short can save you some extra bandwith if you’re abso-
lutely sure that it doesn’t make sense for a user to input queries that are shorter than a number
of characters. Let’s look at a marble diagram that describes Combine’s filter operator. I
want to make sure you’re comfortable with marble diagrams because they are quite common
in the world of FRP and they do a good job of explaining how an operator works if you’re
comfortable reading them:
The implementation we have currently is already quite nice, but I’d like to give you a little
bit more insight into what output our publisher produces at this point. There are a couple of
things we know for sure right now:
Donny Wals 90
Practical Combine
• The label isn’t updated if the text field’s content is too short.
• The label isn’t updated if the user is still typing their search query.
There is one last case that isn’t covered here. What if the user types a query, pauses, then
types some more and quickly deletes the newly typed characters, making the current value
equal to the original value?
Here’s a marble diagram that visualizes what happens when a user does this:
Figure 7: A marble diagram that describes the filter operator with a duplicated output.
Because the label’s text in my example doesn’t change, it’s not easy to see that we handle the
same value twice. We can see this in Xcode by printing the output of the filter operator. We
won’t do this with sink or assign. Instead, we’re going to use the print operator. This
operator will print information about the publisher chain to help you debug and examine
what’s happening:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.filter({ ($0 ?? "").count > 2 })
.print()
Donny Wals 91
Practical Combine
If we go through the same sequence that I’ve just shown in the marble diagram, we’d see the
following output:
Notice that the value Hello is printed twice. This means that the label receives this value
twice.
When updating a label, this isn’t a big deal. But if we’d make network calls, or perform
expensive computation based on the user’s input, this would be quite wasteful. After all,
receiving the same input twice would typically result in the same output. We can prevent this
from happening with the removeDuplicates operator. Before I show you how it’s used,
here’s a marble diagram that describes removeDuplicates:
Donny Wals 92
Practical Combine
As you can see, removeDuplicates will keep any duplicate values to itself if they occur
directly after each other. If a different value is emitted between the first and second occurrence
of a certain value, the values aren’t considered duplicates and they are forwarded to the
subscriber. Let’s see what this looks like in code:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.filter({ ($0 ?? "").count > 2 })
.removeDuplicates()
.print()
.assign(to: \.text, on: label)
Donny Wals 93
Practical Combine
.store(in: &cancellables)
If we’d run through the same sequence of inputs that I’ve presented earlier, you’d see the
following output printed:
This is much better. We won’t waste any precious resources on duplicate values anymore.
Tip: I will revisit this example in Chapter 10 - Debugging your Combine code where
we’ll use Instruments and a tool called Timelane to examine the output of a publisher.
With just three simple operators, we were able to create a search feature that ensures that no
network calls are wasted on queries that are too short or duplicated. We also won’t waste any
network calls by responding to the user input too eagerly. Instead, I showed you how to use
debounce to make sure the user is done typing, or at least took a short break from typing,
before processing their input.
Before I wrap this section up, there is one more output limiting feature that I want to show
you. It’s called throttle.
While debounce resets its cooldown period for every received value, the throttle operator
will emit values at a given interval. When you apply the throttle operator, you can choose
whether you want it to emit the first value received during the throttled interval or the last.
Let’s look at marble diagrams for both:
Emit latest:
Donny Wals 94
Practical Combine
Emit first:
Figure 10: A marble diagram that shows throttle with latest: false
While throttle is somewhat similar to debounce, they serve very different purposes and are
Donny Wals 95
Practical Combine
used in very different ways. When you apply a throttle, all you’re guaranteeing is that a
publisher will not emit values more frequently than specified in your throttle. This means that
if the user is still typing their search query, throttle will output intermediate values while the
user types. If you choose to emit the first value that was generated during a throttle interval,
you might never receive the latest value unless that value was the first value to occur during a
throttle interval. Let’s review the marble diagram with fewer values in it:
Figure 11: A simplified marble diagram that shows throttle with latest: false
Notice how the latest value occurs during the interval. For this reason, it is never emitted to
the subscriber. This may or may not be desirable, depending on the feature you’re building.
For a search field, you definitely want to receive the latest value which means you would pass
true as the value for the throttle’s latest argument. Doing this roughly approaches what
happens when you apply a debounce with the main difference being that the user might still
be in the middle of typing when values are emitted.
I can imagine that throttling is useful for features where you might want to update a value
regularly based on user input. Like for instance, the word count of a document might not need
updating all the time. You might consider once every couple of seconds to be good enough
regardless of whether the user is still in the middle of typing or not. In this case, you could use
a publisher that publishes the document’s current text and throttle its output to compute the
Donny Wals 96
Practical Combine
Donny Wals 97
Practical Combine
Figure 12: Example screenshot of the form we’ll build in this section.
Donny Wals 98
Practical Combine
When the user inputs information, the summary view underneath the text fields updates
dynamically based on the information that the user provides. To achieve this, I want to use as
few publishers as possible. This means that somehow I would need to grab the output from
several form fields, and then funnel these fields into a single publisher so I can subscribe to a
single publisher to update, for example, a user’s full name. Or possibly their entire address.
Consider the following code as a starting point:
I’m sure you can imagine how the text fields from the screenshot map to the publishers in this
code. It’s the same technique that I demonstrated in the first section of this chapter, except
there are more text fields.
The goal that we’re trying to achieve here is to merge the output from the firstName and
lastName publishers into a publisher that emits both values at once and to merge the
address and zipCode publishers as well.
To achieve this, we’ll need one of Combine’s publishers that merges or combines the
output from multiple publishers. When you refer to the documentation for Com-
bine.Publishers, you’ll find that there are three categories of merging publishers:
• Publishers.Zip
• Publishers.Merge
• Publishers.CombineLatest
Let’s go over all three of these publishers, and their different versions to see which one achieves
the outcome that we’re looking for in this example.
Donny Wals 99
Practical Combine
print("send first")
NotificationCenter.default.post(firstNotification)
print("send second")
NotificationCenter.default.post(secondNotification)
print("send third")
NotificationCenter.default.post(firstNotification)
print("send fourth")
NotificationCenter.default.post(secondNotification)
I will not include the notification related code in my code samples since it will be the same
every time.
For Zip, example looks as follows:
Simple enough, right? Let’s see what the output in the console looks like for Publish-
ers.Zip:
send first
send second
(name = first, object = nil, userInfo = nil, name = second, object =
,→ nil, userInfo = nil) zipped
send third
send fourth
(name = first, object = nil, userInfo = nil, name = second, object =
,→ nil, userInfo = nil) zipped
As the example shows, Publishers.Zip is a publisher that takes two publishers, and will
output tuples of the values that are published by the publishers it zips. Notice that for new
values to be emitted by Publishers.Zip, all publishers that it zips must have output a
new value. The values that are emitted by Publishers.Zip are tuples. The number of
values in these tuples is equal to the number of publishers you’re zipping. You can extract the
values in this tuple through their position. So for the value from the first publisher you’d use
val.0, and for the second value you’d use val.1. If only one of the two (or three, or four)
zipped publishers emits a new value, we won’t immediately receive this value. This means
that if one of the two publishers completes before the other publisher is completed, you won’t
get any new values from the uncompleted publisher because the already completed publisher
doesn’t emit new values anymore.
The following marble diagram describes how Publishers.Zip works:
The result of this code is identical to the earlier example. The zip operator has overloads that
allow you to pass to pass two publishers to create a Publishers.Zip3, or three publishers
to create a Publishers.Zip4.
Note that Publishers.Zip will zip the values from its publishers in order. This means that
if one of the publishers emits three values before the second publisher emits a value, the first
value from the first publisher is matched up with the first value from the second publisher.
The following example does a good job of demonstrating this:
left.zip(right).sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
left.value = 1
left.value = 2
left.value = 3
right.value = 1
right.value = 2
(0, 0)
(1, 1)
(2, 2)
As you can see, left emits three values before right emits its first value. Since the pub-
lishers are zipped in order, you can see that Publishers.Zip emits the first and second
emitted value from both publishers together.
Based on the findings from this experiment, we can conclude that Publishers.Zip is not
the publisher we need for the form I showed you at the start of this section. If either the first
name or the last name changes I want to get the current value for both values so I can update
the full name label. We can’t achieve this by zipping because a user would have to change
both fields for a new value to be emitted by Publishers.Zip.
Alternatively, you can use the merge operator, similar to how you were able to use the zip
operator earlier:
If we run the code that I used for Publishers.Zip for Publishers.Merge, the following
output is produced:
send first
name = first, object = nil, userInfo = nil merged
send second
name = second, object = nil, userInfo = nil merged
send third
name = first, object = nil, userInfo = nil merged
send fourth
name = second, object = nil, userInfo = nil merged
As you can see, Publishers.Merge emits a new value every time one of the publishers
it merges emits a new value. Note that it only emits a single value. It interleaves all emitted
values from the publishers that it merges. The following marble diagram describes this:
This is really close to the desired effect. If we’d apply this to the form that I showed you at the
start of this section, we’d be able to update the overview as needed, but we’d have to read the
value of both properties that we’re merging because we wouldn’t be able to figure out which
of the text fields changed since we’d just receive the current value of the property that was
just changed. Let’s look at the last combining publisher. Surely that will do what we need.
This code produces the following output when used in the test skeleton:
send first
send second
(name = first, object = nil, userInfo = nil, name = second, object =
,→ nil, userInfo = nil) combined
send third
This combining publisher is pretty much ideal for our purposes. The only caveat is that both
@Published properties that we’re merging need to have an initial value, which they do. So
let’s use the combineLatest operator to combine the user’s first name and last name, and
populate a UILabel called fullNameLabel:
$firstName
.combineLatest($lastName)
.map({ combined in
return "\(combined.0) \(combined.1)"
})
.assign(to: \.text, on: fullNameLabel)
.store(in: &cancellables)
Because firstName and lastName are @Published properties with initial values, we’ll
imediately begin receiving the user’s input in either of the text fields as soon as one of the
@Published properties changes.
By using the combineLatest operator, you can build some pretty nifty and complicated
features without a lot of code. I won’t give you the code to combine the output from the zip
code and address properties. I’m sure you can write that code by yourself using the example I
just showed you.
In Summary
In this chapter you’ve learned how Combine integrates with a UIKit based user interface. You
saw that Combine doesn’t have built-in support for responding to UIControl events like
changing slider and text field values but you did see how you can assign a slider or text field
value to a @Published property and continue from there.
I then went on to show you how you can take user input, and manage how often and when
you want to process this input using debounce and filter. This is incredibly useful if you’re
building a feature where frequent user input might occur, and where the processing of user
input is expensive. A textbook example of this is a search feature where a user types a search
query that is used to perform a network request. You saw how you can use the print operator
to see exactly what is happening with your publishers. I used this operator to show you that
only debouncing and filtering is not always enough. Sometimes a publisher outputs the same
value multiple times in a row, and I demonstrated that you can ignore duplicate values with
the removeDuplicate operator.
To wrap this chapter up, I demonstrated an alternative to debounce called throttle. This
operator works slightly different from debounce as you discovered through several marble
diagrams. Note that this is the first time I explained an operator with just marble diagrams. I
love how marble diagrams communicate the use of operators in Combine clearly, and concisely
without any noise that could be introduced through code.
In the next chapter you will learn more about networking in Combine, and how you can
implement some modern networking features from iOS nicely with Combine’s operators.
By the end of this chapter, you will have a deep understanding of how you can use Combine
for networking in your application, and you will understand how several of Combine’s more
complex and advanced operators can be used to compose a complex chain of tasks.
completion(.failure(error))
}
do {
let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(T.self, from: data)
completion(.success(decodedResponse))
} catch {
completion(.failure(error))
}
}.resume()
}
And while this code functions just fine, there are a couple of things that could be improved to
reduce the complexity of the code that handles the outcome of the data task. Let’s go over a
couple of things that could use some improvements in this code sample one by one.
First, the data task’s callback handler isn’t very user-friendly. Its type signature is (Data?,
URLResponse?, Error?) -> Void which doesn’t tell us much about how the callback
is supposed to work. In theory, all three arguments could be nil. They might just as well all
be non-nil. There’s just no telling. This means that in our closure, we need to check whether
data is present, and if it isn’t then error should be. I happen to know that they should never
both be nil. But that’s not obvious from the closure arguments.
Then, in the same closure, we need to decode the JSON. This code could be considered noise
because it’s a somewhat specific task and it can generate errors of its own that are different
from networking-related error. In the next section, I will show you how you can use Combine
to decode JSON from a data task, but for now, we’re just going to accept that JSON decoding
is part of the networking code. If you’re not sure what the do {} catch {} part in this
code does, you can read my blog post on throwing functions in Swift to get yourself up to
speed.
Lastly, it’s important to make sure that completion is called on all possible code paths. This
is almost impossible because in production code the assertionFailure would never
trigger which means that in the case that there is no data and no error, the completion
handler is never called. Again, I happen to know that that should never happen so I also know
that this code is fine but it still feels a little off to me.
If you didn’t examine the code sample closely, I want you to take one more look at the
method signature for the function I defined in the example: fetchURL<T: Decod-
able>(_:completion:). This function has a generic type T which is Decodable. This
means that users of the function get to decide what the fetched data is decoded into. This is
very convenient because it enables a very useful level of flexibility.
The completion closure I defined has a signature of (Result<T, Error>) -> Void.
This means that the completion closure is called with a result that contains a decoded model
or an error. If you want to learn more about Swift’s Result type, you can read my blog post on
the topic.
Let’s see what a similar request might look like when we refactor this exact function with
Combine:
This refactored version of fetchURL is much shorter than the version that didn’t use Combine.
It’s also much clearer and more focused on the task at hand than the previous version. By
calling dataTaskPublisher(for:) on the URLSession, a publisher is created that
has a success value of (data: Data, response: URLResponse) and an error type
of URLError. This makes a very clear distinction in terms of what’s expected to happen what
the request succeeds or fails.
If the task fails, the tryMap operator is skipped and the subscriber of the AnyPublisher
that is returned by fetchURL(_:) immediately receives the error. If the request succeeds,
the response data is extracted from the result and its decoded into the generic model that
the caller of this function needs. Because I used a tryMap here, any errors that occur while
decoding the data are automatically forwarded to the subscriber of the resulting publisher.
Note that even if a data task comes back with data and a response, you shouldn’t assume that
the request completed successfully. It just means that your request was sent to the server
successfully, and the server responded. This means that you might have received an error
response from the server. For example, you might need to authenticate for your request, or
you may have requested a resource that doesn’t exist. It’s always a good idea to check the
response’s status code to make sure you got an expected response. This is also true for any
networking code you write without Combine. In the next section of this chapter, I will show
you how you can do this, and how it integrates with Combine.
The networking code that I initially showed you immediately made a request and called a
completion closure when the request was done. In Chapter 2 - Exploring publishers and
subscribers I explained that publishers in Combine don’t perform any work if they don’t
have any subscribers, and URLSession.DataTaskPublisher is no different. When you
call fetchURL(_:) and don’t subscribe to the returned publisher, the network call isn’t
executed.
This means that it’s possible to create a publisher that postpones making a network request
as follows:
And what’s interesting is that you can subscribe to this publisher multiple times to get its
result:
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { (model: MyModel) in
print(model)
}).store(in: &cancellables)
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { (model: MyModel) in
print(model)
}).store(in: &cancellables)
If you were to try this code in a Playground, you’d see that the network request is now executed
twice. Usually, this is the desired behavior for your publisher. However, imagine that you
want to make a single network call that multiple objects subscribe to without making a new
network call. In Combine, you can use the share operator to convert a publisher into a
publisher that has reference semantics. In other words, it converts the existing publisher into
a class instance that republishes its upstream publisher. Let’s see how this operator can be
used in an example:
In this code, I applied the share operator to the publisher that is returned by fetchURL(_:).
This means that the resulting AnyPublisher in this example is now a class instance that
republishes the values that are published by the original AnyPublisher that was returned
by fetchURL(_:). You can use the share operator for more than just networking, it’s
available for all publishers and it allows you to make sure that multiple subscribers for a
single publisher will all receive the same values without triggering more work than needed.
Now that we have the basics of networking down, let’s make the networking code from
this section a little bit fancier by using Combine to decode the data task’s response into the
requested model.
The decode operator works on any publisher that has Data as its output, and it needs
to know what type to decode into, and what kind of decoder should be used to do this.
This decoder must conform to the TopLevelDecoder protocol, and currently, there are
two common objects that conform to this protocol: JSONDecoder and PropertyList-
Decoder. Because URLSession.DataTaskPublisher emits values of type (data:
Data, response: URLResponse), the map operator is needed to extract the data
property from the emitted value.
This part is simple enough, and it’s fairly straightforward. However, networking is often a bit
more complex in the real world. Depending on the response that we get from the network, it
might not make sense to decode into the supplied model. Maybe the server returned a JSON
body that describes an error, along with a non-2xx HTTP status code. By non-2xx I mean a
response with a status code that falls outside of the range of status codes that represent a
success response.
Let’s start simple, if the server returns a response with a non-2xx status code, we’ll decode
the response data into an error model, and we’ll throw an error. If we get a 200 status code,
we’ll decode into the requested model. Let’s set the stage first by changing the logic a little bit
while still only handling the success case:
This first thing to note is the mapError that’s applied to the data task publisher. This step is
somewhat unfortunate, but it’s required to make the flatMap work properly. The flatMap
operator requires that the publisher it operates on has the same error type as the publishers
that it emits. And since decode emits a generic Error, we need to transform the data task’s
URLError to Error. Inside of the flatMap, I don’t do anything fancy. I check whether the re-
ceived response has a status code of 200 and if we do, I return a Just publisher that publishes
the result’s data which is then decoded into T. If you’re not sure what Just is, I recommend
that you go back to Chapter 4 - Updating the User Interface because it’s introduced in that
chapter.
This code is functionally almost the same as code we hade before I introduced the decode
operator. The most important difference is the flatMap operator which just makes everything
a little bit more confusing. So why are we using flatMap here? The reason is simple, I want this
to be complicated on purpose! The flatMap operator sometimes makes you jump through
hoops to get it to work due to its Failure type requirements, and I figured this was a good
opportunity to remind you of that. Towards the end of this section, I will simplify the code a
little bit by removing the flatMap and replacing it with a tryMap instead.
Note that the flatMap emits publishers of type AnyPublisher. The reason for that is that
we’ll have a slightly different publisher for the case where we didn’t get a 200 response, and
even if we did, I don’t want to type out the full publisher returned by applying decode to
Just.
Let’s expand the code I just showed you by adding the code that’s supposed to be where I
initially wrote a fatalError:
The code that I added here is slightly more complex than what we had before. When the
data task comes back with an error code, I still use a Just publisher to grab the data. I
then decode that data into a special APIError struct that models the fields of the JSON
error that I expect, and I then use a tryMap to throw the decoded model as an error. This
makes the Just publisher fail even though it normally doesn’t. Ultimately, the subscribers of
fetchURL(_:) will receive the thrown error in their receiveCompletion block as an
Error object that they can extract from the completion object using the following code:
fetchURL(myURL)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion,
let apiError = error as? APIError {
print(error)
}
}, receiveValue: { (model: MyModel) in
print(model)
}).store(in: &cancellables)
I’m quite happy with how fetchURL(_:) is used but in all honesty, I think the implemen-
tation of fetchURL(_:) is a bit of a mess. You did get to see some more of flatMap, but I
think we can do better. Let’s see what fetchURL(_:) looks like if we don’t flatMap:
This is much cleaner, and much easier to understand don’t you agree? We don’t have to use
mapError, and we don’t have complicated nested publishers that return response.data
which is then mapped using the decode operator. This code doesn’t use the decode operator
at all. Isn’t that something? We started this section with the decode operator, and we end this
section with a tryMap and manual decoding. Pretty much the same thing we did before.
The purpose of this whole exercise was to show you that the tool that looks right for the job
initially, isn’t always the tool that is the right tool for the job.
Let’s explore one last common pattern that I’ve seen in many apps myself, retrying requests
after performing a token refresh.
When you work with an API that requires authentication through tokens, there’s a good chance
that you need to refresh the token every once in a while. In Combine, you can use tryCatch,
map and a new operator called switchToLatest to achieve this kind of behavior. Before
I demonstrate how this is done exactly, I want to show you what switchToLatest does,
and what it’s good for. And to do that, we need to revisit flatMap.
In Combine, flatMap takes the output of a publisher, transforms that into a new publisher and
all values emitted by that new publisher are relayed to subscribers, making it appear as if it’s
a single publisher that emits all of these values. You can limit the number of publishers that a
flatMap will keep active at any time using its maxPublishers argument. When you do this,
flatMap will only transform the first n values that it receives where n is equal to the value you
passed to maxPublishers. Any subsequent values will not be passed to the flatMap until
one of the publishers it created completes and flatMap is ready to handle the next value.
There are times where this isn’t quite what you want to achieve, and that’s what switchTo-
Latest is good for. You can apply switchToLatest to the output of a map operator that
produces publishers. This will automatically emit values produced by the latest publisher that
is produced inside of the map. If you’re familiar with RxSwift, you might know this combination
of operators as a single operator called flatMapLatest.
Now that you have an idea of what switchToLatest does, let’s look at this operator as a
marble diagram:
As you can see, this operator takes values, transforms them into publishers and it only emits
values produces by the latest publisher that was created. The older publisher is discarded.
Let’s see this in action in a final version of the fetchURL(_:) function:
return refreshToken()
.tryMap({ success -> AnyPublisher<T, Error> in
guard success else { throw error }
return fetchURL(url)
}).switchToLatest().eraseToAnyPublisher()
})
.eraseToAnyPublisher()
}
The interesting bit here is the tryCatch operator. The error thrown in tryMap is an APIEr-
ror which has a statusCode property. In my implementation, that statusCode would
match the status code from the HTTP response. The tryCatch operator is an operator you
haven’t seen before but it’s fairly straightforward. It is used to catch any errors that were
thrown upstream, so in this case by the data task itself, or by tryMap. When an error is thrown,
it is passed to the tryCatch, and you can replace the thrown error with a new publisher. In
this case, we only want to replace the error if it’s a 401 error which the HTTP error code for
“unauthorized”. If we get a different error, we immediately throw the error that we received so
it ends up downstream at the subscriber for this publisher.
When we get a 401 error, the refreshToken() function is called which, for demo pur-
poses, returns an AnyPublisher<Bool, Never> that indicates whether the refresh was
successful. If the refresh wasn’t successful, the original error is thrown downstream. If the
refresh completed successfully, the fetchURL(_:) publisher is returned which will cause
the request to be executed again but this time we’d be authenticated. Because we do all of this
in a map, we need to call switchToLatest on the map and erase it to AnyPublisher to
make sure we return an AnyPublisher<T, Error> from the tryCatch.
This whole contraption is quite something, isn’t it? And while we’re doing extremely com-
plicated things here, the code is actually fairly straightforward if you know what all of these
operators do. This is both one of Combine’s strengths and a weakness in my opinion. It’s very
easy to chain operators together and create extremely complicated flows with very little code.
At the same time, the simplicity and terseness of the code can hide complexity in a way that
ultimately makes the code somewhat harder to understand and reason about.
Even though it’s pretty neat, the token refresh flow I just showed you requires with a word of
warning. If your refreshToken function succeeds, but your API still returns a 401 when
you retry the initial request, you might infinitely recurse in this function. In general, this would
probably indicate that something is wrong on your server, but it’s good to be aware of this
caveat.
Tip: In Chapter 12 - Driving publishers and flows with subjects I will show you a slighty
more advanced way to implement a token refresh flow similar to the one I’ve shown
you in this chapter except it uses a PasstroughSubject and flatMap to drive the
process.
When you configure your URLSession like this, any request made by this URLSession
will now error with a URLError that has its networkUnavailableReason set to .con-
strained. For that reason, I wouldn’t recommend enabling low data mode on the con-
figuration for URLSession.shared. Instead, you might want to consider two separate
URLSession instances for this purpose:
This splits the two different kinds of use cases but an approach like this comes with its own
problems. Instances of URLSession can perform certain optimizations for you that aren’t
shared between sessions. This means that the two sessions I just created exist individually
and we might miss out on some networking performance enhancements.
Instead of configuring a URLSessionConfiguration for Low Data mode, it’s also possible
to configure individual requests to respect the user’s Low Data mode settings:
Both of these requests can be executed by the same URLSession. If Low Data mode is active,
constrainedRequest will fail with the error I mentioned earlier. The normalRequest
would execute just fine because it doesn’t opt-in to Low Data mode. Let’s see how this is used
with a URLSession.DataTaskPublisher:
URLSession.shared
.dataTaskPublisher(for: constrainedRequest)
.tryCatch({ error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason == .constrained else {
throw error
}
Based on what you already know about Combine and its operators, this code shouldn’t need
much explanation. We subscribe to the data task publisher, use tryCatch to inspect the
error and forward the error if it’s not a Low Data mode related error, and if it is, we return a
new publisher from tryCatch that will load a lower quality version of the same image.
A feature like Low Data mode is something that I’m sure a lot of your users will come to rely on
to save data when their data plan is low, or when they use a less than ideal WiFi connection. If
you use Combine for networking, I don’t think there’s any reason to not support Low Data
mode in your app. Integrating it is fairly straightforward, and you’d do your users a huge
favor.
Earlier in this section, I mentioned the allowsExpensiveNetworkAccess property that
was introduced in iOS 13. This property doesn’t have a toggle in the OS’ settings, but it’s an
important setting to understand and use. Lots of users use their devices on networks that
are expensive when you use a lot of data. Mobile data plans immediately come to mind, but
metered networks or mobile hotspots might also cost a user a lot of money. If you want to
prevent your app from ramping up quite the bill, make sure to set allowsExpensiveNet-
workAccess to false on your URLSessionConfiguration or URLRequest objects.
This feature is especially useful if your app performs large synchronization operations, or if
your users can download large files from a server using your app.
Both features I mentioned can be used and implemented similarly, so I won’t show how to
implement allowsExpensiveNetworkAccess line by line. Instead, I think you should
be able to implement this on your own.
Note that the homepage in this image is made up of three tasks. One task fetches the featured
content, one fetches a section with curated content and one builds up the user’s favorites
section. The featured section and the curated section are both built using a single network
call. The favorites section is a little bit more complicated. The app that this homepage would
be a part of has some offline capabilities which allow the user to store their favorites locally.
This means that the user might have some favorite items on their device, that are not on the
server. To retrieve the local and the remote favorites, I will use separate publishers that are
then merged into a single publisher using Publishers.Zip. By zipping the remote and
local publishers, I can use a single map to combine both responses into a single set of favorites,
which is then published to the homepage through the merged publisher. Pretty cool, right?
Before I show you the publishers and how they come together in context, I want to show you
the models I’m working with:
struct HomePageSection {
let events: [Event]
let sectionType: SectionType
The model is relatively simple. Each publisher will ultimately map whatever output it has
into a HomePageSection. Even if a fetch operation fails, we should still get an empty
HomePageSection that corresponds with the relevant publisher. This might not be the
best approach to take in a production environment, but for demonstration purposes, it’s more
than good enough.
Let’s start with the two simple publishers. The curated content and the featured content
publishers:
Both of these publishers retrieve an array of [Event] from the server, which is used to create
the relevant HomePageSection objects. This code uses several mechanisms that you are
already familiar with. The replaceError operator is very important for the flow that I’m
building in this section. It makes sure that no section ever comes back with an error. In this
case, decoding error and networking errors will be caught and replaced with an empty events
array.
Let’s look at the two publishers that will make up the favoritesPublisher next:
class LocalFavorites {
static func fetchAll() -> AnyPublisher<[Event], Never> {
// retrieve events from a local source
}
}
var remoteFavoritesPublisher =
,→ URLSession.shared.dataTaskPublisher(for: curatedContentURL)
.map({ $0.data })
.decode(type: [Event].self, decoder: JSONDecoder())
.replaceError(with: [Event]())
.eraseToAnyPublisher()
These publishers on their own don’t look very impressive. They are quite simple and similar
to the two publishers you saw before. The code so far shouldn’t contain any surprises but
we’re about to start with the fancy work. Let’s zip up the two favorites publishers:
By zipping the two favorites publishers together, it’s guaranteed that both publishers will
have retrieved data when the map operator is called. In the previous chapter you learned
that Publishers.Zip emits tuples with the values from the zipped publishers. In this case,
that means that we have two tuple members and both are [Event] objects. We can add
them together and convert them to a Set to remove duplicates, and then convert back to an
array to create the favorites HomePageSection.
With this favoritesPublisher in place, we’re ready to create the homePagePub-
lisher which will merge the favoritesPublisher, curatedPublisher and
homePagePublisher
.sink(receiveValue: { section in
switch section.sectionType {
case .featured:
// render featured section
case .curated:
// render curated section
case .favorites:
// render favorites section
}
}).store(in: &cancellables)
None of the code I have shown you in this section is extremely complex or completely new
to you. But by cleverly combining what you already know you can come up with complex
flows in Combine that read naturally, and are reasonably simple to understand. It’s those
moments when you write a bunch of small, isolated pieces of code that can be composed
together using built-in operators to create something complex and beautiful when I truly
appreciate everything that Combine enables me to do without thinking about all the complex
and intricate details that are under the hood of Combine’s operators.
In Summary
In this chapter, you took your networking knowledge to the next level. I’ve shown you several
interesting concepts that apply to networking and Combine. You saw how you can build a very
simple and basic networking layer in Combine, and you saw how Combine deals with JSON
decoding beautifully using the decode operator. You also learned how you can implement a
rather complicated networking flow where you attempt to refresh an authentication token if a
network request failed due to an authentication error, and automatically retrying the network
request if the token refresh succeeds.
After that, I showed you how you can integrate Low Data mode in you app to make your app’s
networking logic more user-friendly. It was particularly interesting to see how you can use
tryCatch to inspect a networking error and create a new publisher to make a new network
request if needed.
To wrap this chapter up, I showed you how you can use Combine’s combining publishers to
build a complex and interesting flow of network calls that all fetch a section of a homepage.
You learned that you can write small isolated bits of code to build simple publishers that
can be composed into something far more complex. With all of this newfound knowledge
you didn’t just expand your networking skills. All of Combine’s operators and combining
publishers can be used in many contexts that might not involve any networking at all.
createFuture()
.sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
In this code, you can already derive a lot of information about how a Future is used. The
createFuture() function creates an instance of a Future that has Int as its Output
and Never as it’s Failure. This means that we’ll emit a single Int from this Future, and
the Future will never fail.
The initializer for Future takes a closure. This closure will be passed a Promise closure. This
Promise closure must be called by you with an instance of Result to fulfill the Promise,
which will make the Future emit its result. The Result that you pass to the Promise
must have the same Success and Failure types as the Future’s Output and Failure.
Makes sense so far, right?
You can perform all kinds of work in the closure that’s passed to the Future’s initializer. In
the initial example, I just generate a random Int but we could do far more elaborate work,
like making a network request:
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
This code creates a Future that is used to perform a network request. When the request
completes, I check whether the request is successful and I fulfill the Promise with the re-
spective success or failure value. If you look closely at how fetchURL(_:) is used in this
example, you should see that it looks very similar to the networking code that I’ve shown you
before. In the previous chapter you saw the following code:
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { (model: MyModel) in
print(model)
}).store(in: &cancellables)
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { (model: MyModel) in
print(model)
}).store(in: &cancellables)
I explained that subscribing to a publisher twice would result in the URL request being executed
twice. Once for each subscriber. The reason this happens is that publishers normally don’t
emit values or perform work if they don’t have any subscribers. This is not how a Future
works in Combine.
A Future begins executing its work immediately when it’s created. Try placing the following
code in a Playground to see what I mean:
import Combine
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
print("RECEIVED RESPONSE")
}.resume()
}
}
You’ll notice that RECEIVED RESPONSE is printed to the console even though you never
subscribed to the created Future.
Now add the following code after the let publisher = fetchURL(myURL) line:
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value.data)
}).store(in: &cancellables)
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value.data)
}).store(in: &cancellables)
if you run this code, you’ll find that the following output is printed to the console:
141745 bytes
finished
141745 bytes
finished
RECEIVED RESPONSE
This proves that a Future will not only execute regardless of whether you subscribe to it.
It also shows that a Future will only run once. Once a Future is completed, it will replay
its output to new subscribers without running again until you create a new Future. This is
important so I’ll make it extra clear:
The following code would execute the Future-based network call twice:
fetchURL(myURL)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value.data)
}).store(in: &cancellables)
fetchURL(myURL)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value.data)
}).store(in: &cancellables)
The reason the code above executes the network call twice is that every time you call
fetchURL(_:) a new Future is created. This Future will begin executing immediately
and will emit a single value to its subscribers.
Because a Future executes once and begins its execution immediately when its created, it
could lead to some interesting and confusing outcomes if you’re not careful. When you call
eraseToAnyPublisher() on a Future, it becomes an AnyPublisher. This means
that users of your Future will not be able to tell that they’re working with a Future that
begins its execution immediately, emits a single value and re-emits the same value to its
subscribers if it receives more than a single subscriber.
It’s possible to force futures to wait for subscribers before they start their execution. This will
make your futures behave more like other publishers, and it gives back some control over
when a Future executes its work. You can do this with the Deferred publisher.
Let’s look back at the first example of a Future I showed you:
You know that this Future will run immediately when you call createFuture(). To
prevent this using the Deferred publisher, you would have to refactor createFuture()
as follows:
Note that both of these statements are false for a regular Future. The following code demon-
strates this:
plainFuture.sink(receiveValue: { value in
print("plain1", value)
})
plainFuture.sink(receiveValue: { value in
print("plain2", value)
})
deferred.sink(receiveValue: { value in
print("deferred1", value)
})
deferred.sink(receiveValue: { value in
print("deferred2", value)
})
plain1 2015758617746974568
plain2 2015758617746974568
deferred1 1272179517968986878
deferred2 2449688451723054346
Notice how plain1 and plain2 show the same integer value. The reason for this is that the
Future executed immediately, and repeats its output. The deferred version prints a different
value for each subscription. The reason for this is that the Deferred publisher executes
the code that it receives as a closure whenever it receives a new subscriber. In the example
where I introduced Deferred, I returned an instance of Future in the Deferred closure.
This means that every subscriber of deferred in the example above indirectly subscribes to
a new instance of Future rather than subscribing to the same Future instance multiple
times.
In other words, wrapping a Future in Deferred makes it behave exactly the same as any
other Publisher.
So when should you use Deferred and when shouldn’t you use Deferred? That’s a tough
question in my opinion. The decision is ultimately a semantic and API design decision and
the answer will vary for each use case. If you’re certain that performing work immediately
and only once is the way to go, a plain Future is probably fine. An example of this could be
loading an app’s configuration file if that file never changes throughout the course of your
application.
Wrapping a Future in Deferred makes it behave like a normal Publisher. This could
be a good fit if you don’t want a Future to execute immediately, or if you want to be able to
subscribe multiple times to the same instance of Deferred without getting the same result
every time. In my opinion, it’s a good idea to evaluate the behavior you need on a case by
case basis while keeping in mind how your code is used.
Note that if you’re working on an API that is meant to be used by others and you feel like
you can’t make the choice between a plain Future or one that’s wrapped in a Deferred
publisher, you can let the user of your API decide for themselves. They can wrap a call to a
function that creates a Future in Deferred themselves if needed:
your code that the publisher they receive will behave like any other Publisher in Combine
without being aware of it being a deferred Future. This is an implementation detail that can
be hidden, allowing you to change the underlying publisher without having to change the
return type.
All of these small details about futures and deferred futures are extremely important to un-
derstand, and I would like to advise you to use futures with care, and avoid hiding them in
your codebase. If you’re going to use a Future, it needs to be clear and obvious that you are
doing so to avoid confusion down the line. That said, there are interesting applications for
futures in Combine. Let’s look at a couple of fun examples.
UNUserNotificationCenter.current().getNotificationSettings { settings
,→ in
switch settings.authorizationStatus {
case .denied:
DispatchQueue.main.async {
// update UI to point user to settings
}
case .notDetermined:
UNUserNotificationCenter.current().requestAuthorization(options:
,→ [.alert, .badge, .sound]) { result, error in
A lot is going on in this example, and you might consider this code somewhat hard to read
and follow. If you examine the code closely, there are two parts to what’s happening here.
First, we check for the user’s current notification settings. This is an asynchronous task. In
the completion handler for this task, we examine the current notification settings and based
on that we take action. If the current authorizationStatus is .notDetermined, the
user has never been asked for push permissions before so we need to ask them for permission.
This is another asynchronous task with a completion handler. Depending on the permission
status, we take the same actions that we would if we didn’t ask for permission first.
The current implementation for this code is very explicit and somewhat wordy. It’s also less
than ideal that two paths lead to the same outcome of this code which is to present the user’s
notification permissions in the UI.
There are two asynchronous tasks in this code:
extension UNUserNotificationCenter {
func getNotificationSettings() -> Future<UNNotificationSettings,
,→ Never> {
return Future { promise in
self.getNotificationSettings { settings in
promise(.success(settings))
}
}
}
I can’t stress it enough but futures in Combine are meant for one-off, immediately executing
tasks. This means that as soon as requestAuthorization(options:) is called, the
closure passed to the Future immediately executes even if we don’t subscribe to the created
Future yet, and its result is broadcast to all future subscribers. For some operations this is
good. In the case of requestAuthorization(options:) we wouldn’t want to trigger
the Future more than once even if we’d subscribe to the Future multiple times.
The two methods I added to UNUserNotificationCenter in the extension I just presented
fit well within the example that I want to show you, and in my personal opinion that fit well
for the concept I want to demonstrate. Whether or not a Future truly is the correct choice
depends on your app, your requirements, and your goal. What matters to me most is that
you understand the implications and details of using a Future over a Subject or other
Publisher.
The two methods that I created can be used to rewrite the imperative code I showed earlier as
follows:
UNUserNotificationCenter.current().getNotificationSettings()
.flatMap({ settings -> AnyPublisher<Bool, Never> in
switch settings.authorizationStatus {
case .notDetermined:
return UNUserNotificationCen-
,→ ter.current().requestAuthorization(options: [.alert,
,→ .sound, .badge])
.replaceError(with: false)
.eraseToAnyPublisher()
case .denied:
return Just(false).eraseToAnyPublisher()
default:
return Just(true).eraseToAnyPublisher()
}
})
.receive(on: DispatchQueue.main)
.sink(receiveValue: { hasPermissions in
if hasPermissions == false {
// point user to settings
} else {
// we have permission
}
}).store(in: &cancellables)
The new version of the code is not necessarily shorter than the initial version. It is, however,
cleaner and less repetitive. There is now a single place where the UI is updated and by applying
a flatMap, we can ask the user for notification permissions within our chain of operators
quite neatly.
Notice that I used a new operator called receive in this example. The receive operator
allows you to specify where you want to receive values emitted by a publisher. In this case, we
want to work with the UI when we receive the user’s notification permissions, and there’s a
good chance that we won’t receive the user’s notification permissions on the main thread if we
don’t explicitly call receive. I will explain receive in-depth in Chapter 8 - Understanding
Combine’s Schedulers.
Let’s look at another cool example of how a Future can be used to wrap Core Data fetch
requests.
extension NSPersistentContainer {
func fetchUsers(using moc: NSManagedObjectContext, _ completion:
,→ @escaping ([User]) -> Void) {
moc.perform {
let request: NSFetchRequest<User> = User.fetchRequest()
let users = try? moc.fetch(request)
completion(users ?? [])
}
}
}
struct UsersFetcher {
let persistentContainer: NSPersistentContainer
}
}
The code in this example isn’t very special and if you’re familiar with Core Data this should look
somewhat familiar. We can refactor this code to work with a Combine Future and remove
all completion closures fairly easily:
struct UsersFetcher {
let persistentContainer: NSPersistentContainer
Notice how little code we had to change to get a Future based implementation. By wrapping
the entire moc.perform block in a Future, we end up with a Combine-friendly version of
fetchUsers(_:).
Keep in mind that even though Combine hides a lot of complexity, you still need to keep Core
Data’s threading confinements in mind if you abstract your fetch requests into futures. In this
case, it would be good enough to use the UsersFetcher like this to avoid any problems:
usersFetcher.fetchUsers()
.receive(on: DispatchQueue.main)
.sink(receiveValue: { users in
// handle users
})
This might not always be the case so thread as careful as you normally would if you decide to
add Future based abstractions to Core Data.
In Summary
In this chapter, I have shown you how Combine’s Future works. You learned that a Future
is a Publisher but that it doesn’t behave like publishers you have seen so far. Futures in
Combine mainly differ from publishers because a Future begins executing its work immedi-
ately when it’s created, and it replays its results to all subscribers that subscribe to the same
Future. This is not always true for a Publisher. Furthermore, a Future is guaranteed
to always produce a single value or error. This can’t be said for all objects that conform to
Publisher.
You saw how this behavior makes Future a good fit for abstracting existing asynchronous
work that uses completion handlers in your app. I demonstrated this with two examples. In
one example I showed you how you could wrap two methods from UNUserNotification-
Center to get a user’s current notification permissions and to ask for permission if needed.
You also saw how you could abstract a Core Data fetch request using a Future.
There are far more applications of futures that are feasible than I can reasonably cover in a
single chapter. That’s why I chose two examples that do a good job of showing you how to
refactor existing asynchronous code to work with a Future. It’s more important to focus on
the refactoring process I’ve attempted to show you than it is to focus on the exact application
of Future because, as I said, there are many, many more applications of Future that you
could come up with.
• RunLoop
• DispatchQueue
• OperationQueue
In Chapter 5 - Using Combine to respond to user input, you encountered schedulers for the
first time when I showed you this code:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.assign(to: \.text, on: label)
.store(in: &cancellables)
In this example, Combine needs a scheduler to set up some timed work to make the debounc-
ing work. Because timers and delayed tasks are inherently tied to schedulers, there is no way
we could schedule our debouncer without passing a scheduler to the debounce operator.
Note that in this case, DispatchQueue.main is likely to be an okay choice, but there is a
potential problem.
Timers are paused when the main queue is busy. For example, if you’re running a normal
timer and begin scrolling through a table view, the timer will be paused until the scroll ends.
In this case, it’s better to schedule the timer on a different queue than the main queue. The
same appears to apply to schedulers in Combine. So in this case, we could use the following
code if we want to make sure the main thread doesn’t interfere with the debouncing logic:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.global())
.assign(to: \.text, on: label)
.store(in: &cancellables)
getting this wrong. There is a thread on the Swift forums that you can go through for more
information. Overall, I think it’s important that you understand that Combine uses schedulers
internally but there is typically no need to get down to the details unless you have to.
Now that you have an idea of what Combine’s Scheduler protocol is, let’s take a closure
look at a common use case.
UNUserNotificationCenter.current().getNotificationSettings()
.flatMap({ settings -> AnyPublisher<Bool, Never> in
switch settings.authorizationStatus {
case .notDetermined:
return UNUserNotificationCen-
,→ ter.current().requestAuthorization(options: [.alert,
,→ .sound, .badge])
.replaceError(with: false)
.eraseToAnyPublisher()
case .denied:
return Just(false).eraseToAnyPublisher()
default:
return Just(true).eraseToAnyPublisher()
}
})
.receive(on: DispatchQueue.main)
.sink(receiveValue: { hasPermissions in
if hasPermissions == false {
// point user to settings
} else {
// we have permission
}
}).store(in: &cancellables)
I used receive(on:) here because I wanted to make sure that my subscriber would receive
all of its values on the main queue so I could safely update the UI.
By default, Combine applies a default scheduler to the work you do. The default scheduler
will emit values downstream on the thread that they were generated on. Let’s explore this
behavior through an example:
intSubject.sink(receiveValue: { value in
print(value)
print(Thread.current)
}).store(in: &cancellables)
intSubject.send(1)
DispatchQueue.global().async {
intSubject.send(2)
}
queue.addOperation {
intSubject.send(3)
}
Examine the code in this snippet closely to see how I’m using three different origins to send
value over the PassthroughSubject. If you would run this code in a Playground, you
would see the following output:
1
<NSThread: 0x6000019da180>{number = 1, name = main}
2
<NSThread: 0x6000019d2500>{number = 7, name = (null)}
3
<NSThread: 0x6000019c6e80>{number = 3, name = (null)}
It’s clear that the sink’s receiveValue is called on a different thread every time. This is a
problem if we want to use the PassthroughSubject to drive UI. In Chapter 4 - Updating
the User Interface I didn’t give this detail too much attention because I thought it would
distract from the main idea of that chapter. Now that you’re more comfortable with Combine,
it’s time to start paying more attention to the finer details which is why I’ve saved this topic
for now.
When you apply receive(on:) to a publisher it will make sure that all events are delivered
downstream on the scheduler you provide. For example, you could modify the subscription
from the previous example to always receive values on the main queue as follows:
intSubject
.receive(on: DispatchQueue.main)
.sink(receiveValue: { value in
print(value)
print(Thread.current)
}).store(in: &cancellables)
I would recommend that you only apply the receive(on:) operator right before the sink
or if you absolutely need something to happen on a specific thread.
The other operator I mentioned earlier, is the subscribe(on:) operator. This operator
works it’s way upstream through the publisher stream and modifies the scheduler that is
used for the publisher’s subscribe, request and cancel operations. Note that this does not
mean that you can set the scheduler that upstream publishers output their values on. Just
the thread that their subscribe method is called on. To give you an idea, let’s look at an
interesting example of how this works exactly with a publisher chain that’s similar to the one
you’ve used before:
intSubject
.subscribe(on: DispatchQueue.global())
.sink(receiveValue: { value in
print(value)
print(Thread.current)
}).store(in: &cancellables)
intSubject.send(1)
intSubject.send(2)
intSubject.send(3)
}
}
generateInt()
.map({ value in
sleep(5)
return value
})
.sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
print("hello!")
In this example, a random integer is generated using a Future. For the sake of testing, I
use the map operator to do something expensive which is faked by calling sleep to pause
execution for five seconds. If you run this code, you’ll find that nothing happens for five
seconds, then the generated integer is printed, and then "hello" is printed.
This situation is less than ideal. We can do much better by applying a subscribe(on:)
operator:
generateInt()
.map({ value in
sleep(5)
return value
})
.subscribe(on: DispatchQueue.global())
.sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
If you apply the subscribe(on:) operator after the map, you’ll find that "hello" is
printed immediately. This is much better than the old situation. Try moving the sub-
scribe(on) around to see what happens:
generateInt()
.subscribe(on: DispatchQueue.global())
.map({ value in
sleep(5)
return value
})
.sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
If you’d run the code like this, the effect is the same. The reason for this is that sub-
scribe(on:) affects how objects subscribe to a publisher which means that it affects
the entire chain of publishers and operators. Note that in this case, the future’s work is
never affected by subscribe(on:). The reason for this is that a Future performs
work before it has subscribers which means that the work it does can’t be affected by
subscribe(on:).
It’s possible to mix subscribe(on:) and receive(on:) to make sure your subscrip-
tions are done off the main thread but values are received on the main thread:
generateInt()
.subscribe(on: DispatchQueue.global())
.map({ value in
sleep(5)
return value
})
.receive(on: DispatchQueue.main)
.sink(receiveValue: { value in
print(value)
}).store(in: &cancellables)
Even though Combine’s operators to manipulate threading and queueing behavior of your
publishers are simple, it doesn’t magically make threading easy. Use these operators with
care and only if you need them. It’s far too easy to make bad assumptions about threading
and cause problems in your app. Regardless, it’s good to be aware of these operators because
they can really help you clean up your code, and they allow you to take control over what
happens where if needed.
One last thing I want you to take note of is that publishers get to make their own decisions
about the queue they use to emit values on. A lot of publishers will emit values on the queue
they received their subscriber on, but this is not always the case. For example, try the following
code to see what happens:
URLSession.shared.dataTaskPublisher(for: URL(string:
,→ "https://practicalcombine.com")!)
.subscribe(on: DispatchQueue.main)
.map({ result in
print(Thread.current.isMainThread)
}).sink(receiveCompletion: { _ in }, receiveValue: { value in
print(Thread.current.isMainThread)
})
Even though we subscribe to the data task on the main thread, both the sink and map aren’t
called on the main thread. The reason for this is that the DataTaskPublisher is set up to
publish its values off the main thread at all times. I can only assume that this is done because
URL requests are always performed off the main thread to avoid blocking the UI for a long
time.
In Summary
In this chapter, I explained what the Scheduler protocol Combine is used for, and I gave
you a rough overview of what a scheduler is. I explained that several built-in objects like
DispatchQueue and Runloop conform to Scheduler and that you can use them to tell
Combine where certain code should be executed.
Then I went on to show you how you can use Combine’s receive(on:) and sub-
scribe(on:) operators using several examples. You learned that receive(on:) allows
you to specify on what scheduler values should be delivered downstream from your call to
receive(on:). This is especially useful to make sure that your sink receives values on
the main queue so you can safely update your UI.
extension Publisher {
func customSink(receiveCompletion: @escaping
,→ (Subscribers.Completion<Self.Failure>) -> Void,
receiveValue: @escaping (Self.Output) -> Void) ->
,→ AnyCancellable {
return AnyCancellable(sink)
}
}
I cannot stress enough that the code I present in this chapter is my interpretation that will
usually work as intended. This code is not tested thoroughly enough to call it perfect, nor is it
how sink is implemented in Combine.
Notice that I call self.subscribe(sink) in this code snippet. The subscribe method
from Publisher is where the magic is. It’s where Combine connects a subscriber and
publisher to each other to set up a subscription stream. Inside of a publisher’s subscribe
method is where a publisher will start its work, and where the stream will get going. Let’s
examine subscribe a bit more.
There are multiple implementations of subscribe define on Publisher. One is meant
to attach subjects like PassThroughSubject to a publisher. Doing this will automatically
forward any events emitted by a publisher to the subscribing subject. Let’s look at a brief
example:
subject
.sink(receiveValue: { receivedInt in
print("subject", receivedInt)
}).store(in: &cancellables)
publisher
.subscribe(subject)
.store(in: &cancellables)
Because subject in this code is directly subscribed to publisher, all values emitted by
publisher are automatically republished by subject. While this is pretty cool and could
be useful in certain cases, this isn’t the subscribe method that is used in the custom sink
I showed you earlier. In that method, I use the other version of subscribe which takes a
Subscriber as its argument.
The documentation for subscribe isn’t particularly helpful at the time of writing this chap-
ter. But if you examine the documentation for Publisher, you’ll find some interesting
information about subscribe.
The documentation mentions a method called receive(subscriber:). This method
is a required method on the Publisher protocol, and it’s called whenever any version of
subscribe is called on a publisher. The documentation also mentions that a publisher
uses its receive(subscriber:) method to receive new subscribers and to call several
methods on the subscriber to interact with it:
These three methods happen to be methods that are required by the Subscriber proto-
col.
extension Subscribers {
class CustomSink<Input, Failure: Error>: Subscriber {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
init(receiveCompletion: @escaping
,→ (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}
subscription.request(.unlimited)
}
This code defines an extension on Subscribers and adds a new class called CustomSink
to the Subscribers enum. I do this to make my custom subscriber fit in with Combine’s
other subscriber objects nicely. The CustomSink is generic over an Input and a Failure.
The Input will match up with the Output of a Publisher, and the Failure will match
up with the Failure type of the Publisher we’re subscribing to. Just like the regular
Subscribers.Sink, the custom version will take two closures. One for incoming values,
and one for incoming completion events. Note that my custom sink’s receive(_:) and
receive(completion:) call these closures directly. This will allow the custom sink’s
behavior to match that of the built-in one.
In receive(_:) I return a value of .none. I will explain that in a moment, let’s look at
receive(subscription:) first. In this method, I store the received Subscription
object. The reason for this is so we can call cancel on the subscription, or cancel it if needed.
Note that I also call subscription.request(.unlimited) here. This matches up with
the definition of Subscribers.Sink which states the following:
A simple subscriber that requests an unlimited number of values upon subscription.
You’ve seen the concept of requesting or demanding values a couple of times now, and
you’ve also seen an object called Subscribers.Demand which is the return type of re-
ceive(_:). This concept is one of Combine’s driving forces, and it’s called backpressure
or backpressure management. In the next subsection, I will explore this topic with you. But
before we do, I want to wrap up the custom sink that I just created because it’s not quite
ready.
Remember that the following line from the customSink method I implemented earlier?
return AnyCancellable(sink)
All we do here is cancel the subscription, and set it to nil to prevent memory leaks. The
CustomSink can now be used as follows:
extension Publisher {
func customSink(receiveCompletion: @escaping
,→ (Subscribers.Completion<Self.Failure>) -> Void,
receiveValue: @escaping (Self.Output) -> Void) ->
,→ AnyCancellable {
return AnyCancellable(sink)
}
}
There’s hardly a difference with the initial version of customSink. The only thing that I’ve
changed is that sink is now an instance of Subscribers.CustomSink which is exactly
what I was going for. Now that the custom subscriber is finished, let’s explore the concept of
backpressure before we move on to creating a custom publisher.
Understanding backpressure
Backpressure is a feature in Combine that is extremely important to make everything work
the way it does, but it’s also a feature that’s hidden from users of the framework relatively
well. You can get pretty far with Combine without realizing that backpressure even exists, but
if you want to understand how Combine works, and possibly create custom subscribers or
even publishers, you must understand backpressure.
In Combine, subscribers are in charge of the number of items they receive, if any at all. By
requesting, or demanding, values from a subscription object, a subscriber can communicate
to a subscription whether it’s prepared to receive values. This can help you prevent your
system from backing up completely if a subscription is sending a subscriber more values than
it can handle.
The way it works is that a subscriber will use its receive(subscription:) method to
request an initial number of items. In the case of Subscribers.Sink, this is an unlimited
number of items. This means that Subscribers.Sink will receive all values published by a
publisher, no matter how rapidly those values are emitted, or how many values are generated,
until the subscription is canceled. This kind of unlimited demand is represented through
the Subscribers.Demand.unlimited object. Alternatively, we could request an initial
demand of .none which means no values at all or .max(Int) where Int represents a
maximum number of values equal to the value that’s passed to it. To communicate an initial
demand of a single value, we could write the following:
This code would result in the subscriber receiving a single value, and then no further values
until the subscriber calls subscription.request(_:) again with a new demand, or if
it returns a new demand larger than .none from its receive(_:) method.
This is perfectly fine because I already requested an unlimited number of values from the
subscription. Returning .none from receive(_:) does not negatively affect the initial
demand from the subscriber. Demands in Combine can only be increased. You can never
decrease a subscription’s demand. This means that you can’t lower the number of items you
want to receive by requesting something like .max(-1).
If you have an initial demand that isn’t .unlimited, but for example, you requested
.max(1), you can return a new demand from receive(_:) as follows:
This code will request one more value from the subscriber every time it received a value.
A subscription will only send values to a subscriber if demand is high enough. This means
that if you’d request an initial number of .max(10) values, and always return .none from
receive(_:), your subscription will stop receiving new values after the tenth item is deliv-
ered to your subscriber. It’s important to note that this means that you will also not receive
any completion events from the publisher you’ve subscribed to if the subscriber’s demand
doesn’t allow this. A subscriber can call request(_:) on a subscription at any time to
communicate that it’s ready to receive new values.
This contract of how and when a subscriber will or will not receive new values due to back-
pressure is extremely important and it’s a huge part of why Apple recommends that you don’t
create custom publishers. Apple’s publishers are supposed to honor all of the implicit and
explicit details of how publishers send values to their subscribers, and the corresponding
subscription objects are perfectly tuned to work together with their publishers as needed.
Getting backpressure management right in a custom publisher is no easy task but we’re going
to explore it regardless because I want to show you the basics of how publishers can be im-
plemented so you can make your own choices when the time comes to decide whether you
should create a custom publisher.
ints
.sink(receiveValue: { print($0 )})
.store(in: &cancellables)
ints
.sink(receiveValue: { print($0 )})
.store(in: &cancellables)
The full array of integers will be presented both times you subscribe to ints. Based on this,
it’s safe to say that a publisher’s completion state isn’t directly tied to the publisher itself.
The subscription that’s created by the publisher is ultimately responsible for initiating and
emitting a stream of values. A publisher is just an object that acts as some kind of placeholder,
or facade for whatever happens internally. This is why Future is such an interesting publisher
because it doesn’t follow the same narrative. When you subscribe to a Future, it executes
immediately, and it executes only once.
To see how this publisher of integers might work, let’s implement a custom version of it!
extension Publishers {
struct IntPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S)
where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription =
,→ Subscriptions.IntSubscription(numberOfValues:
,→ numberOfValues,
subscriber:
,→ subscriber)
subscriber.receive(subscription: subscription)
}
}
}
This publisher isn’t terribly exciting, is it? The publisher I’ve defined publishes integers and
never fails. The most interesting bit is that in the receive(subscriber:) method I create
a new subscription object, and pass it to the subscriber. Let’s look at the implementation of
my Subscriptions.IntSubscription to see if it’s any more exciting:
extension Subscriptions {
class IntSubscription<S: Subscriber>: Subscription where S.Input ==
,→ Int, S.Failure == Never {
let numberOfValues: Int
var currentValue = 0
var subscriber: S?
currentValue += 1
openDemand -= 1
}
if currentValue == numberOfValues {
subscriber?.receive(completion: .finished)
cancel()
}
}
func cancel() {
subscriber = nil
It certainly is a lot more code, and the interesting bit is the request(_:) method’s imple-
mentation.
In it, I store the new value for the demand that we need to fulfill. This will be equal to the
current demand, plus the newly requested demand. Because demands are always additive
and we can use the + operator on two demands, it’s easy to add the new demand to the
existing demand.
Because this is a simple published and all work can be done synchronously, I used a while loop
to send values to the subscriber that was passed to this subscription’s initializer. This loop
will run until we have no open demand anymore, or until we’ve published all of the values
we needed to publish. When we call subscriber?.receive, the subscriber can update
its demand, so we need to add this new demand to the open demand. We then increase the
current value by one, and the open demand is decreased by one because we just sent a value
to the subscriber. After the loop, I check whether all values were sent to the subscriber. If
this is the case, the subscription is completed and the subscriber is informed. If not, we’ll
have to hold off on sending new values to the subscriber until it requests them from this
subscription.
This example of creating a custom subscription and publisher is purely intended to give you
an idea of how they work. They are not intended to be used as a definitive reference of how
publishers and subscribers work exactly, and they are not intended to be used as a definitive
reference on how to perfectly manage backpressure in a subscription.
Let’s look at a final code snippet that uses all of the custom objects I’ve created in this sec-
tion:
customPublisher
.customSink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { int in
print(int)
})
.store(in: &cancellables)
Try playing around with this code and the custom sink to see how the custom publisher adapts
to different kinds of demand strategies. It works pretty well, especially if you consider it for
what it is. It’s just a silly example to help you grasp the concept of backpressure management
and to give you an idea of how publishers are created.
Even though we’re not supposed to create custom publishers, and backpressure management
is super complicated if you want to get it right all the time, I want to show you an example of
a custom publisher that I think you’ll like. It’s a publisher that will allow you to subscribe to
UIControl events with a very nice API which will allow you to improve some of the code
I’ve shown you in Chapter 4 - Updating the User Interface.
slider
.publisher(for: .valueChanged)
.sink(receiveValue: { control in
guard let slider = control as? UISlider
else { return }
print(slider.value)
}).store(in: &cancellables)
extension UIControl {
struct EventPublisher: Publisher {
typealias Output = UIControl
typealias Failure = Never
}
}
extension UIControl {
class EventSubscription<S: Subscriber>: Subscription
where S.Input == UIControl, S.Failure == Never {
control.addTarget(self,
action: #selector(eventOccured),
for: event)
}
func cancel() {
subscriber = nil
control.removeTarget(self,
action: #selector(eventOccured),
for: event)
}
extension UIControl {
func publisher(for event: UIControl.Event) ->
,→ UIControl.EventPublisher {
It’s that simple! Crazy, right? And you can now use publisher(for:) in the exact way I
mentioned at the start of this section. You can even apply all of Combine’s operators on this
custom publisher. For example, you might want to use debounce to prevent this publisher
from firing new values all the time while the user is dragging a slider:
slider
.publisher(for: .valueChanged)
.debounce(for: 0.2, scheduler: DispatchQueue.main)
.sink(receiveValue: { control in
guard let slider = control as? UISlider
else { return }
print(slider.value)
}).store(in: &cancellables)
Creating custom publishers can be really powerful, and a lot of fun. Regardless, I think it’s
good to take Apple’s advice and only create your own publishers when there is no other way
to reasonably achieve the functionality you need. This might sound very conservative, but
publishers and subscriptions can be complex beasts and backpressure management can be
hard to get right, especially for more complex asynchronous tasks.
In Summary
What a chapter this was. Everything you’ve learned about Combine came together. I hope I
was able to make the final pieces of the puzzle fit in your mind by giving you an idea of how
Combine works behind the curtain.
In this chapter, you have learned how subscribers, publishers, and subscriptions work. You
learned why publishers don’t start doing work until they have a subscriber that requests
sufficient demand, and you’ve learned what demand is and how it works. You learned about
one of Combine’s hidden cornerstones which is its backpressure mechanism.
I’ve shown you how you can mimic Combine’s built-in sink method, and then I went on to
show you how you can create an extension on UIControl to create a custom publisher that
allows you to write beautiful and convenient code.
In the next chapter, you will learn one last valuable skill. You will learn how you can debug
and profile your Combine publishers through the built-in print operator, and a tool called
Timelane.
[1, 2, 3].publisher
.print()
.flatMap(maxPublishers: .max(1), { int in
return Array(repeating: int, count: 2).publisher
})
.sink(receiveValue: { value in
// handle value
})
In the previous chapter you learned a lot about subscriptions so this output should be much
more meaningful than the first time I showed it to you. You can see when our subscription is
created, how many values the subscriber requests and what outputs are produced. Note that
this output is somewhat complicated because it involves a flatMap with a maxPublishers
of one. This means that the flatMap now acts as a subscriber that requests a single value from
its upstream publisher initially. It also requests a single new value every time the publisher
that’s created within the flatMap completes. While this is somewhat complex, it all makes
sense when you know what’s happening.
If you have no idea how flatMap works, this output might not be extremely useful. Moreover,
in many real applications, you will have multiple publishers running at the same time and
your code might log a lot more data than we did in this simple example. Luckily, there’s a tool
available that can help us debug and understand Combine code in a much more convenient
way.
you will learn how you can install and use this super valuable tool. I will guide you through
the installation process (which is fairly straightforward) and we’ll look at some neat examples
to see Timelane in action.
After installing the Instruments template, you can go ahead and open Xcode. The easiest way
to integrate Timelane is through the Swift Package Manager. Open the project you want to
use Timelane in and navigate to File -> Swift Packages -> Add Package Dependency.
Figure 19: Screenshot of the Xcode menu to access Swift Package Manager
In the pop-up that appears, enter the TimelaneCombine Github URL which is:
https://github.com/icanzilb/TimelaneCombine
Figure 20: Screenshot of the Add Package screen with the TimelaneCombine URL prefilled
Adding this package to your project will automatically pull down and install the Time-
laneCombine package in your project. If you’re using Cocoapods or Carthage to manage
your dependencies you can add the TimelaneCombine dependency to your Podfile or
Cartfile as needed.
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
This code is used to prevent the $searchQuery publisher from emitting too many values in
a short time. However, it had a problem that we discovered using the print operator. Let’s
use the lane operator this time to discover the same problem:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.lane("Search query")
.assign(to: \.text, on: label)
.store(in: &cancellables)
By applying the lane operator after debounce, you can inspect every event that is emitted
by the debounce operator. This means that we’ll see all the debounced values. The string
that is passed to the lane operator is used to label the lane that’s used to display the output
of debounce. To use Timelane for debugging, you need to run your app through Instruments.
You can do this by pressing cmd+i or through Xcode’s menu by going to Product -> Profile.
When Instrument launches, make sure to select the Timelane template:
Figure 21: A screenshot of the Instruments template selector with Timelane selected
When you run your app with Instruments, your Combine code is profiled and visualized in
realtime. This means that you can inspect your subscriptions in great detail. If you run the
above code and you type something, remove a character and retype that character, the
Instruments log might look as follows:
First, notice the big green line in the top Instruments lane. This line resembles our subscription
to the $searchQuery publisher. The line starts when the subscription is created (which is
immediately) and it runs until the subscription is completed. I stopped the recording session
after I captured a couple of values so the line ends. In reality, this line would run until the
point where the subscription is destroyed because this specific never completes on its own.
In the second lane, you can see values over time. This lane represents the values that are
emitted. In the bottom section of the Instruments window, you can see more details about
the emitted events. Notice that the output Timelane is duplicated. This is because I typed
Timelane, waited a moment, removed a letter and then added the removed letter back. I
can demonstrate this by adding an extra lane operator:
$searchQuery
.lane("Raw search query")
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.lane("Search query")
.assign(to: \.text, on: label)
.store(in: &cancellables)
Notice that there are two lanes now. One for each time the lane operator is used. You can see
that the Raw search query lane shows all the characters I typed without debouncing them.
The Search query lane is debounced and shows fewer values because it only displays values
that are emitted by the debounce operator. A lane operator only works on the publisher
that it is applied to. This is very convenient because it allows you to gain really good insights
into the values that travel through your publisher chain.
If you’re working with many different lanes, you might want to limit the data you track in
Instruments. In the case of $searchQuery, you might not be interested in seeing its sub-
scription lifecycle. If this is the case for you, you can apply a filter to the data that is logged by
Timelane. This means that you can control whether you want a certain lane to show up under
Subscriptions only, Events over time only or both. You can apply a filter as follows:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.lane("Search query", filter: [.event])
.assign(to: \.text, on: label)
.store(in: &cancellables)
By passing [.event] to the lane operator, I can tell it to only log values that are emitted
and ignore the subscription lifecycle. If you’d want this to work the other way around and
only see the subscription lifecycle, you can pass [.subscription] to the lane operator
instead. Let’s look at an example session for the code I just showed you:
Figure 24: A screenshot of a Timelane session that only tracks events for the created lane
Notice how there are events logged, but the top lane of the Instruments session is empty. This
is extremely useful when you’re working with lots of lanes and want to minimize the noise in
your Instruments sessions.
Speaking of noise, because we’re sending optional values to Instruments, they get logged as
Optional("Timelane"). This isn’t ideal and we can improve this using a third argument
for the lane operator:
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.lane("Search query", filter: [.event], transformValue: { value in
return value ?? ""
})
.assign(to: \.text, on: label)
.store(in: &cancellables)
In this example, I use the transformValue argument and a closure that unwraps the
optional value into the value or an empty string to get rid of the Optional part of the
output. You can use the transformValue argument to perform all kinds of transformations
on the data your logging. Imagine that you’re working with some complex values like user
models. You can use transformValue to extract and log a specific property of the user
instead of logging the entire object:
aPublisherThatEmitsUsers
.lane("Users", filter: [.event], transformValue: { user in
return "User with id: \(user.id)"
})
The ability to transform values is extremely useful and it can help you produce a nice and
clean Instruments log that only contains the output you need.
In addition to using Timelane through the lane operator, it also has a dedicated property
wrapper that allows you to debug @Published properties. Whenever you have a property
that is defined as follows:
This will log every value that is emitted by the $searchQuery to a lane that’s labeled as
Search query. Since @Published publishers often live for a long time and you might not be
interested in visualizing their subscription lifecycle, you can apply a filter to them as follows:
I added a call to the lane operator at the end of every publisher from the section in Chapter
6 - Using Combine for networking where we built this complex chain of network calls. I will
not repeat all of the code in this chapter so if you want to refer to the code you can look at
Chapter 6 - Using Combine for networking, or you can have look at the sample code for this
chapter in the book’s code bundle. When you run Timelane to visualize the complex flow of
subscriptions in this example, you get the following output:
Figure 26: A screenshot of a Timelane session for the complex network chain from Chapter 6
Notice how all publishers create their subscriptions at the same time. They all finish as soon
as their work is done, and the Homepage publisher lane doesn’t complete until the Curated
publisher completed because that’s the last publisher to complete. Once the last publisher
completes, the home page publisher can merge the output from the curated, featured and
favorites publishers. Note that the Favorites publisher lane completes together with the
Remote favorites publisher because it depends on the local and remote favorites to emit
values. While this is really cool, let’s see what happens if one of the steps in this complex
process fails:
Figure 27: A screenshot of a Timelane session for the complex network chain from Chapter 6
where one task fails
This output is extremely interesting. The Featured publisher failed before the Curated pub-
lisher could complete. This causes the Curated publisher to be canceled, and the Homepage
publisher completes with an error. The curated publisher is canceled because it doesn’t make
sense for that publisher to do any work once one of the publishers that the homepage pub-
lisher is supposed to merge failed. When one of the three publishers that the homepage
publisher depends on fails, it will emit that error immediately, and the stream is completed.
The fact that the curated publisher is canceled because it was still running when the featured
publisher failed is something you probably would not have discovered without Timelane and
its powerful visualizations.
In Summary
In this chapter, you learned a lot about debugging with Combine. You saw how the print
operator provides quick insights into what a publisher and subscription do exactly. You also
saw how you can gain more structured, deep insights into your code through Timelane. By
analyzing and visualizing your code, you can gain valuable insights that can help track down
and fix problems. I like Timelane a lot for debugging and I wouldn’t be surprised if Apple adds
native support for the kind of debugging that it provides in the near feature.
Now that you know how to debug your Combine code, there is just one more thing I want to
show you in this book. Being able to write unit tests is an important skill for developers so in
the next chapter you will learn how to write unit tests for your Combine code.
By the end of this chapter you will have a good understanding of what it means to test Combine
code. You will not just learn how to write tests, but you will also have an understanding of
what you should and what you should not test when you’re using Combine. The last thing
you want in a test suite is to have unit tests that don’t test the logic you think you’re testing
because it’s testing the wrong thing.
Note: In this chapter I will assume that you have basic knowledge of unit testing for iOS.
I will briefly explain some basics, but if you’ve never written a unit test before there’s a
good chance you will feel lost while going through this chapter. In that case, I would like
to recommend that you take a look at the testing section on my blog. Specifically I would
like to recommend Getting started with unit testing on iOS – part 1 and Getting started
with unit testing on iOS – part 2.
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
}
Imagine that we wanted to write a test for this model. Take a moment to examine the model
and ask yourself what you’d like to test here. Or in other words, what guarantees would you
like to be able to make about this code.
A good test I can think of is to make sure that no mutations or side-effects are applied when a
new value is assigned to kwhInBattery. Additionally, I’d want to make sure that every new
value I assign to kwhInBattery is sent to subscribers of the $kwhInBattery publisher.
Note that this last test sits right on the border of what we should, and should not test. The
reason it’s on the border is that we should not be testing whether Combine does its job. Since
func testKwhBatteryIsPublisher() {
let newValue: Double = 10.0
var expectedValues = [car.kwhInBattery, newValue]
car.$kwhInBattery.sink(receiveValue: { value in
// we'll write the assertion logic here
}).store(in: &cancellables)
car.kwhInBattery = newValue
This test contains two properties, one to hold the Car instance that’s being tested and another
to store the AnyCancellable objects that are created during the test. In the test’s setUp
method these properties are initialized with fresh instances. This ensures that every test we
write has a fresh Car and set of AnyCancellable to work with. In the testKwhBat-
teryIsPublisher test method, I create an array of expected values. These are the values
that I expect to receive in my sink. If everything works as expected, I should receive the
default kwhInBattery value first, and my newValue second. I’ve added these values to
the expectedValues array in the order that I expect the values to be emitted in.
I also create an XCTestExpectation using XCTestCase’s expectation(description:)
method. This method creates an expectation object that must be fulfilled to consider the
test completed. Notice the last line in the test method where I call waitForExpecta-
tions(timeout:handler:). Calling that method will pause execution of the test until
all XCTestExpectation instances I have created are fulfilled. In this case, there’s only
one and we’ll fulfill it in the sink I used to subscribe to $kwhInBattery.
In the sink’s receiveValue closure I will ensure that the received value is equal to the
fist item in the expectedValues array. If it is, then the publisher emitted the value that I
expected. Once I’ve established this, I will update the expectedValues array by removing
the first element from the expectedValues array using its dropFirst() method. By
doing that the first item is removed from the array, moving each element up one slot. So the
next time the receiveValue closure is called, the first element in the expectedValues
array should match the value that’s emitted. I will keep updating the expectedValues
array and comparing the emitted element to the first item in expectedValues until it’s
empty. If the test receives a value once the expectedValues array is empty, the publisher
emitted more values than expected.
Let’s fill in the sink method and write some actual test code:
car.$kwhInBattery.sink(receiveValue: { value in
guard let expectedValue = expectedValues.first else {
// This creates a new array with all elements from the original
,→ except the first element.
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
receivedAllValues.fulfill()
}
}).store(in: &cancellables)
This code follows the steps I described earleir. First, I make sure that I have an expected value
to compare the received value with. If I don’t have any expected values left, the sink received
more values than expected. Next, I check whether the expected value matches the received
value. If it does, I update expectedValues by removing the first item from the array of
expected values. If I removed the last value from that array, the receivedAllValues
expectation can be fulfilled and the test is considered completed.
Because the expectedValues array contains two items, I expect the sink to be called
twice before considering the test completed.
Apart from some details, this code really isn’t all that different from code you would write to
test other asynchronous code. It ultimately comes down to verifying that a certain closure is
called with the expected arguments a certain number of times before considering the test to
be completed.
What’s important to take away here is that I don’t test whether I can map over the published
values or transform them otherwise. If you write a map in your test, it’s likely that you are
accidentally testing whether Combine’s map works rather than testing that your code does
what it should.
When you write a test for your Combine code you want to make sure that your publishers emit
the values that they should emit. Not whether you can multiply that value by two using a map,
and apply a filter to remove any values larger than some arbitrary number in your test. A
test like that wouldn’t verify that your code is correct at all. It would just verify that Combine
can do its job.
Luckily, I have an example of a scenario like that. Remember the CarViewModel from
Chapter 4 - Updating the User Interface? It’s okay if you don’t. I included the code for that
view model below:
struct CarViewModel {
var car: Car
car.kwhInBattery -= kwhNeeded
}
}
Notice how batterySubject creates a publisher that converts the Double values pub-
lished by $kwhInBattery to String. If we want to test this logic, we should not write
a test that applies map to $kwhInBattery. Instead, we should test batterySubject
directly and test whether the strings emitted by batterySubject are the strings we need.
Here’s what that test might look like:
func testCarViewModelEmitsCorrectStrings() {
let newValue: Double = car.kwhInBattery - car.kwhPerKilometer *
,→ 10
var expectedValues = [car.kwhInBattery, newValue].map {
,→ doubleValue in
return "The car now has \(doubleValue)kwh in its battery"
}
carViewModel.batterySubject.sink(receiveValue: { value in
//we'll write the assertion logic here
}).store(in: &cancellables)
carViewModel.drive(kilometers: 10)
carViewModel and verify that after calling drive, my batterySubject emits an updated
status string.
Similar to the earlier example, I create an array of expected output. This time, I map over the
Double values I expect to be generated by the car and transforming them into strings. Note
that this doesn’t violate any principles I mentioned before. I trust that Swift’s map works on
arrays and I use it to generate my expected test output. That’s different than writing a test
that validates that applying map to an array produces the expected output.
The most important bit of this test is written inside carViewModel.batterySubject.sink.
Everything else is basically the same as the previous test. I update my expected output,
check whether the emitted value matches my expected value and I fulfill the expecta-
tion if all values are received. Let’s look at the implementation of the assertion logic in
carViewModel.batterySubject.sink.
If you want, you can try to finish this test yourself before looking at my code. The implementa-
tion should look almost identical to the assertion logic from the earlier test.
carViewModel.batterySubject.sink(receiveValue: { value in
guard let value = value else {
XCTFail("Expected value to be non-nil")
return
}
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
receivedAllValues.fulfill()
}
}).store(in: &cancellables)
The only difference with the code from before is that I added an extra guard to make sure
the emitted value is not nil.
Notice that even though we’re making sure that the CarViewModel maps the Double
values emitted by $kwhInBattery to String, we don’t explicitly rely on map. We don’t
even check whether map is used. Or whether the String values are derived from the Car’s
$kwhInBattery publisher. All we’re interested in is making sure that batterySubject
emits the expected strings. To achieve this I used the same techniques that I would use to test
any other asynchronous code.
class ImageLoader {
var images = [URL: UIImage]()
var cancellables = Set<AnyCancellable>()
init() {
let notificationCenter = NotificationCenter.default
let notification =
,→ UIApplication.didReceiveMemoryWarningNotification
notificationCenter.publisher(for: notification)
.sink(receiveValue: { [weak self] _ in
self?.images = [URL: UIImage]()
})
.store(in: &cancellables)
}
}
The code itself doesn’t do much other than listening for memory warnings and clearing the
images dictionary when the system sends a memory warning.
If you’ve given some thought to how you would test this code you may have come up with an
idea similar to this:
If you would write this test there’s a good chance it’ll work just fine. But there’s also a good
chance that at some point down the line you’re going to introduce some unexpected behav-
ior.
After all, every object in your app has access to NotificationCenter.default. And by
that I don’t mean every object in your code. It also means that every object from UIKit,
Combine and any third-party dependency you use has access to NotificationCen-
ter.default.
So while sending a notification through the default Notification Center might work fine for a
while it’s not unlikely that other parts of your code will react to the memory warning that’s
sent by your test and it’s possible that this introduces a bug in your test.
Or worse, maybe the system actually sends a memory warning in the middle of you setting
up your test. What would happen if you receive a memory warning before you’ve set up
everything you wanted to test. And what happens when you fire your memory warning after
the system has issued its memory warning? You don’t want to worry about these things.
Instead of trying to make NotificationCenter.default work, I would refactor Im-
ageLoader as follows:
class ImageLoader {
var images = [URL: UIImage]()
var cancellables = Set<AnyCancellable>()
I haven’t changed much. All I did was allow users of ImageLoader to inject a specific Noti-
ficationCenter instance if they desire to do so. I use NotificationCenter.default
as the default value for this argument so I can still create instances of ImageLoader with-
out passing a Notification Center explicitly because I really only want to inject a different
Notification Center in my tests.
By using a special Notification Center in my tests I have fine-grained control over when I send a
memory warning, and more importantly, I know that no external code can listen for or trigger
memory warnings on that specific Notification Center instance.
Try to write a test for this object yourself before peeking at my version:
import XCTest
import Combine
import UIKit
@testable import Chapter11
let memoryWarning =
,→ UIApplication.didReceiveMemoryWarningNotification
notificationCenter.post(Notification(name: memoryWarning))
return image
})
.eraseToAnyPublisher()
}
The way I wrote this code makes it virtually impossible to reliably test it. This code is tightly
coupled to URLSession.shared. This means that for loadImage(at:) to load an
image we need a network connection. And the server that we would load images from in the
test has to be up and running. And not just that, the server must have an image at the location
we want to test. If any of these three preconditions are missing, any test that we write for
loadImage(at:) would fail, even if our logic in loadImage(at:) is perfectly fine.
It would be a shame if a unit test fails for reasons that are outside of our control. Unit tests are
supposed to tell you when your code is faulty, not that a server somewhere is down. Before
you can write a test for loadImage(at:) we’ll need to do some refactoring.
Usually in an application you’ll want to abstract all network access behind a networking layer.
This means that the ImageLoader should not use URLSession.shared.dataTaskPublisher(for
directly. Instead, it should access a method on an object that implements networking op-
erations that are useful in your app. To make it possible to create a fake networking object
for testing purposes, it’s common to write a protocol that your networking implementation
conforms to. This will allow you to use the protocol in your code rather than a concrete
implementation of a networking object.
For the ImageLoader, I came up with the following networking protocol:
protocol ImageNetworking {
func loadURL(_ url: URL) -> AnyPublisher<Data, Error>
}
This protocol is very basic but it’s also rather important. Notice that the loadURL(_:)
function in this protocol returns AnyPublisher<Data, Error>. A URLSes-
sion.DataTaskPublisher has (data: Data, response: URLResponse)
as it’s Output and URLError as its Failure. By not copying this Output and Failure
in my protocol I have completely decoupled my networking protocol from URLSession.
Now that we have this networking protocol, let’s define an object that ImageLoader can
use to load image when we’re not using it in a test suite:
class ImageLoader {
var images = [URL: UIImage]()
var cancellables = Set<AnyCancellable>()
self.network = network
Okay, at this point the ImageLoader depends on an object that implements the Ima-
geNetworking protocol. By default we’ll use an instance of ImageNetworkProvider
but we can use a different object when testing the ImageLoader. Let’s update loadIm-
age(at:) so it uses this new ImageNetworking object:
return network.loadURL(url)
.tryMap({ data in
guard let image = UIImage(data: data) else {
throw ImageLoaderError.invalidData
}
return image
})
.eraseToAnyPublisher()
}
The code looks fairly similar to what we had before but it’s far more testable. Now that
ImageLoader is no longer tied to URLSession, we can use any object we’d like in our
test suite to act as the networking layer as long as it implements the ImageNetworking
protocol.
There are three things I would like to test regarding the ImageLoader and networking. First,
I want to make sure that the ImageLoader uses the networking object that we pass it to
make network requests for new images. Second, I want to test that the ImageLoader caches
loaded images in its images dictionary. And third, I want to test that the ImageLoader
uses its cached images instead of the network if a cached image is available.
To do this, I will write two tests. One will verify that the networking object is used and im-
ages are written to the cache. The second test will verify that cached images are used when
available.
Before I show you any tests, I want to show you the mock ImageNetworking object that’s
used in the test suite. If you write mock objects in your own test suite, make sure to add them
to your test target. They have no business in your application target.
return Just(data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
This mock network object is very simple. It keeps track of whether loadURL(_:) has been
called, and when it’s called I create a simple publisher that returns the data for a UIImage
instance. It doesn’t matter that this example uses an SF Symbol image instead of a more
useful image. After all, we just want to test whether ImageLoader works as expected. Not
that our mock image loader provides the correct images to ImageLoader. With this helper
in place, add the following property and setUp method to ImageLoaderTests:
I will use some Combine in the tests I’m about to show you so each test will need a fresh set of
AnyCancellable objects.
The first test I want to show you is the test that verifies that ImageLoader uses the network
object we give it and that it caches images that it loads. If you’re feeling adventurous, try
implementing this test on your own. You have seen all the bits and pieces needed to complete
this task.
If you’re not sure how you would tackle this kind of test, no worries. Here’s what I came up
with:
func testImageLoaderLoadsImageFromNetwork() {
// setup
let mockNetwork = MockImageNetworkProvider()
let notificationCenter = NotificationCenter()
let imageLoader = ImageLoader(notificationCenter, network:
,→ mockNetwork)
// expectations
let loadCompleted = expectation(description: "expected image load
,→ to complete")
let imageReceived = expectation(description: "expected to receive
,→ an image")
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
This test follows a similar pattern to what you’ve seen before. It’s just a bit bigger.
First, I create all objects needed to perform my test. A special networking object, an isolated
Notification Center and the ImageLoader itself. I also set up some test expectations. One to
validate that we eventually recieve an image, and one to validate that the AnyPublisher
returned by ImageLoader eventually completes successfully.
Next, the actual test is performed. I call loadImage(at:) on the ImageLoader. At that
point I expect the ImageLoader to do its work. When I receive an image, I just fulfill the
imageReceived expectation. I don’t care about the exact UIImage I received. All I care
about is that I received something. In receiveCompletion, I check that the task completed
with success. I also check that the ImageLoader used my mock network object and that
the ImageLoader has stored an image in its images dictionary using the URL I requested
as a key. Lastly, I fulfill the loadCompleted expectation.
If you’re following along, go ahead and run this test.
What’s that?
The test failed?
Good! We used unit testing to find a bug!
The ImageLoader does not handle caching of images yet. Its loadImage(at:) method
needs some tweaking to work properly.
Currently, loadImage(at:) uses the ImageNetworking object to load an image and
then applies a tryMap to convert the fetched data to an image if possible. You might be
tempted to add a simple line of code to this tryMap:
self.images[url] = image
But doing so would violate the rules of tryMap. We’re supposed to take the input passed
to tryMap, attempt to transform it and return the transformed value or throw an error. We
shouldn’t apply any side-effects in a tryMap.
Luckily, Apple realized that sometimes you need to apply side-effects. And to help us keep
our code proper, Combine has a special operator that’s perfect for applying side-effects. This
operator is called handleEvents.
When you apply the handleEvents operator to a publisher, you can hook into virtually every
stage of a publisher’s lifecycle. You can react to the publisher receiving a subscriber, you can
respond to errors or handle any values that are emitted by the publisher. The handleEvent
operator has many optional arguments like receiveSubscription, receiveRequest,
receiveOutput and more. To see them all, type handleEvents after a publisher in
Xcode and let it autocomplete the operator for you.
In this case, we want to respond to the values that are emitted by tryMap so we can cache
the UIImage instances that are created by tryMap. To do this, we use handleEvents
and pass it a receiveOutput closure:
return network.loadURL(url)
.tryMap({ data in
guard let image = UIImage(data: data) else {
throw ImageLoaderError.invalidData
}
return image
})
.handleEvents(receiveOutput: { [weak self] image in
self?.images[url] = image
})
.eraseToAnyPublisher()
}
The handleEvents operator is perfect for cases like this where you want to sit in between
the publisher you created and its subscriber. By using handleEvents, the ImageLoader
can now handle every UIImage that’s emitted alongside the code that subscribes to the
publisher that subscribed to the AnyPublisher returned by loadImage(at:). Note that
handleEvents does not count as a subscriber. This means that a publisher will not start
performing work when you call handleEvents on it. You still need to explicitly subscribe
to the publisher to set up a proper subscription.
After updating loadImage(at:), the test we wrote earlier should pass.
There’s one more test I want to write. This test will make sure that ImageLoader uses its
cached image when possible:
func testImageLoaderUsesCachedImageIfAvailable() {
// setup
let mockNetwork = MockImageNetworkProvider()
let notificationCenter = NotificationCenter()
let imageLoader = ImageLoader(notificationCenter, network:
,→ mockNetwork)
let url = URL(string: "https://fake.url/house")!
// expectations
let loadCompleted = expectation(description: "expected image load
,→ to complete")
let imageReceived = expectation(description: "expected to receive
,→ an image")
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
This test is almost identical to the previous test. The only difference is that I populate the
image cache with an image using the URL I’m testing with as a key. Then in the receive-
Completion closure I make sure that mockNetwork.wasLoadURLCalled is false. In
other words, I make sure that the ImageLoader did not attempt to load the image from the
network because it had the image in its cache.
With the tests I showed you in this section, you should now have an idea of how you can
abstract code in a way that allows you to substitute external dependencies like Notifica-
tionCenter or the network using objects that you can control in your test. Sometimes
this means you simply create an isolated instance of an object. Other times it’s a little bit
more involved and you can only obtain the required level of control by hiding the external
dependency behind a protocol.
The ultimate goal here is to create a controlled environment that you can set up and manipulate
in your test suite so you can be sure that you’re only testing the code and logic that you want
to test. And that any test successes or failures are the result of your code, and not the result of
events that occurred outside of your control.
In the next section, I will show you some helpers that I think can be useful when you’re testing
Combine code by making your life somewhat easier.
func testCarViewModelEmitsCorrectStrings() {
let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * 10
var expectedValues = [car.kwhInBattery, newValue].map { doubleValue
,→ in
return "The car now has \(doubleValue)kwh in its battery"
}
carViewModel.batterySubject.sink(receiveValue: { value in
guard let value = value else {
XCTFail("Expected value to be non-nil")
return
}
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
receivedAllValues.fulfill()
}
}).store(in: &cancellables)
carViewModel.drive(kilometers: 10)
There is a whole bunch of boilerplate in this test. You can tell because the test you wrote in
CarTest closely resembles the CarViewModel test. It would be really nice to capture this
boilerplate somehow so the tests themselves can be much cleaner. To do this, you need to
think about what it is that these tests do and have in common.
Ultimately both tests have a set of expected values that should be emitted by a publisher.
Next, I subscribe to the publisher that should emit these values, compare the emitted value
with an expected value and ultimately the test is considered completed if all expected values
are received.
When I refactor code to be less repetitive I usually try to imagine how I would like to use my
code after it has been refactored. For this example I came up with the following design for
CarViewModelTest:
func testCarViewModelEmitsCorrectStrings() {
let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * 10
var expectedValues = [car.kwhInBattery, newValue].map { doubleValue
,→ in
carViewModel.batterySubject
.assertOutput(matches: expectedValues, expectation:
,→ receivedAllValues)
.store(in: &cancellables)
carViewModel.drive(kilometers: 10)
This code is much shorter than before, and it also communicates our intent much clearer.
We want to assert that the values published by carViewModel.batterySubject match
the values in expectedValues. This code is concise, expressive and if you ask me, it looks
kind of beautiful. The implementation of the assertOutput operator looks as follows:
return self.sink(receiveCompletion: { _ in
// we don't handle completion
}, receiveValue: { value in
guard let expectedValue = expectedValues.first else {
XCTFail("The publisher emitted more values than expected.")
return
}
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
expectation.fulfill()
}
})
}
}
func testImageLoaderLoadsImageFromNetwork() {
// setup
let mockNetwork = MockImageNetworkProvider()
let notificationCenter = NotificationCenter()
let imageLoader = ImageLoader(notificationCenter, network:
,→ mockNetwork)
let url = URL(string: "https://fake.url/house")!
// expectations
let loadCompleted = expectation(description: "expected image load
,→ to complete")
let imageReceived = expectation(description: "expected to receive
,→ an image")
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
If you recall I showed you two virtually identical tests in the previous section. One to verify
that the ImageLoader attempted to load an image from the network and cached the result
and one to verify that the ImageLoader would use a cached image instead of attempting to
load one from the network if a cached image is available.
This code could be improved if we could somehow tell the loader to load an image, get a
Result<Success, Failure> object back, and then perform any assertions that we
need to do.
It’s probably easier to understand what I mean if I show you:
func testImageLoaderLoadsImageFromNetwork() {
// setup
let mockNetwork = MockImageNetworkProvider()
let notificationCenter = NotificationCenter()
let imageLoader = ImageLoader(notificationCenter, network:
,→ mockNetwork)
let url = URL(string: "https://fake.url/house")!
That looks clean, doesn’t it? We can assert that the image load was successful using
XCTAssertNoThrow(try result.get()) and check whether we accessed the
network and if the image is now cached without subscribing to anything or even using any
XCTestExpectation. So how does this magic work? Before I show you I want to give
a quick shout out to John Sundell and Cassius Pacheco for giving me some pointers and
sharing their ideas that led me to create the following helper:
extension XCTestCase {
func awaitCompletion<P: Publisher>(for publisher: P) ->
,→ Result<[P.Output], P.Failure> {
let finishedExpectation = expectation(description: "completion
,→ expectation")
var output = [P.Output]()
var result: Result<[P.Output], P.Failure>!
_ = publisher.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
result = .failure(error)
} else {
result = .success(output)
}
finishedExpectation.fulfill()
}, receiveValue: { value in
output.append(value)
})
return result
}
}
Summary
In this chapter, I have shown you everything you need to know to begin testing your Combine
code. You know that you should not test whether Combine’s map does what it should but
instead that you should test whether a publisher emits the number of elements you expect
it to emit given a certain input. You learned that this fits really well in Combine’s Functional
Programming roots where you expect every chain of function calls to produce a certain output
for a given input.
You also learned how you can use XCTest’s expectation API to write asynchronous tests that
help you validate whether a publisher has emitted all expected elements and/or errors. I
also showed you a convenient helper that’s inspired by code from John Sundell and Cassius
Pacheco that you can use to wait for a publisher to complete before asserting that all produced
elements are correct.
class DataFetcher {
private var currentPage = 0
This example is super simple and basic and you might have implemented the same feature in
a completely different way. That’s okay. The point isn’t to make this the perfect fetcher for
you. It’s to show you how you can convert a loader like this to work with Combine.
To do that, we need to think of a nice API.
We could make loadNextPage return an AnyPublisher<Data, Error>, and that
would work. However, that means that we need to subscribe to the publisher that’s created
by loadNextPage every time we want to load new data. But what if we’d want to trigger
loadNextPage in a place where it’s not convenient for us to handle its result. In an infinitely
scrolling list for example. Instead, what we can do is use a PassthroughSubject<Data,
Error> to publish newly fetched data, and we could make loadNextPage return nothing.
If this sounds familiar, you’re probably thinking of the DataProvider that I showed you in
Chapter 4 - Updating the User Interface.
In this chapter, I’m using a slightly different, more generic object called DataFetcher but
it’s built on the same building blocks as the data fetcher you’ve seen before.
class DataFetcher {
private var currentPage = 0
func loadNextPage() {
let url = URL(string:
,→ "https://practicalcombine.com?page=\(currentPage)")!
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.sink(receiveCompletion: { _ in },
receiveValue: { [weak self] loadedData in
self?.dataPublisher.send(loadedData)
self?.currentPage += 1
})
.store(in: &cancellables)
}
}
Looks good?
Not really. As you might recall from Chapter 4 - Updating the User Interface, I don’t like
subscribing to a publisher in loadNextPage(). You already saw one way to get rid of the
sink in loadNextPage() using assign(to:) and a @Published property. But what
if I don’t want to use @Published and keep my PassthroughSubject?
In Chapter 3 - Transforming publishers you learned about flatMap. You saw that flatMap
takes values that are emitted by a publisher and converts these emitted values into new
publishers, where all values emitted by the publishers created in the flatMap are emitted to
a single subscriber.
To achieve my goal of not having to subscribe to anything in loadNextPage, I need a
flatMap.
Why? You ask.
I’m glad you asked.
In your application, hardly anything happens without an external force doing something.
Usually, this external force is the user. The user taps buttons, scrolls in lists, locks their device,
visits locations in the real world that might trigger updates in your app, and performs many
other actions with your app and their device.
If you think of these actions as streams of values, where a value could be the act of scrolling
or needing a different page, you’re essentially thinking of a publisher that emits actions as
values.
When you have a publisher that emits values, you can flatMap over these values to create
new publishers that do something. So in the case of our infinitely scrolling list, requesting
a page could be an event that’s emitted by a publisher. And the publisher that loads
the next page could be the result of applying flatMap to the page requesting publisher.
When we slap a map on the publisher created in the flatMap (which emits URLSes-
sion.DataTaskPublisher.Output) we can extract data. This results in a publisher
that emits <Data, URLError> which happens to line up with the Output and Failure
of my dataPublisher property.
Let’s refactor DataFetcher so it implements the flow I described above:
class DataFetcher {
private var currentPage = 0
func loadNextPage() {
loadRequestPublisher.send(())
}
}
In the code above you can see how I defined a PassthroughSubject that has Void as its
Output. This subject’s only purpose is to emit values that can be flatMapped over. Every time
loadNextPage is called a new Void value is emitted by loadRequestPublisher.
I have updated dataPublisher to be a lazy property that’s initialized with a closure. This
publisher is now the result of taking loadRequestsPublisher, applying flatMap on it
to create a new data fetch task, and mapping over the output of the flatMap to extract the
data property from the URLSession.DataTaskPublisher.Output. The resulting
publisher is erased to AnyPublisher.
The result of the code above is that once dataPublisher has a subscriber, every value emit-
ted by loadRequestPublisher triggers a new data fetch. The best part is that there are
no subscriptions being set up within DataFetcher and its loadNextPage() method.
There is just one problem with this code though. The currentPage property is
never updated. I could refactor the map that currently only extracts data, make it a
closure where I update self.currentPage and return the data from the URLSes-
sion.DataTaskPublisher.Output that’s passed to the map but that violates the idea
that map shouldn’t operate on any external resources. It should be pure.
Instead, we can use the handleEvents operator. This operator allows us to hook into the
lifecycle of a subscription, without subscribing to it. It’s ideal for applying side-effects. If you
need a refresher on handleEvent please refer back to Chapter 11 - Testing code that uses
Combine. The following code shows an updated version of the loadRequestPublisher
that uses handleEvents to update the currentPage property:
})
.eraseToAnyPublisher()
}()
A setup like the one I just showed you is pretty advanced, and it might take a little while
before the concept of using subjects and flatMap like this sinks in (pun intended) and you
feel confident enough to design your own flows that use a similar pattern.
That said, let’s push this train forward and see how we can refactor this paginated
DataFetcher and make it load all pages recursively until there are no more pages without
having to call loadNextPage every time.
struct ApiResponse {
let hasMorePages: Bool
let items: [String]
}
A real response would of course be far more complex than this, but this should be enough to
build our recursive data loader.
I’m also going to assume that we’re using a networking object that takes care of loading and
returning pages. I’m not going to make real network calls, so my mock networking object uses
a Just publisher to return pages when needed.
Here’s what the mock network object looks like:
class NetworkObject {
func loadPage(_ page: Int) -> Just<ApiResponse> {
if page < 5 {
return Just(ApiResponse(hasMorePages: true,
items: ["Item", "Item"]))
} else {
return Just(ApiResponse(hasMorePages: false,
items: ["Item", "Item"]))
}
}
}
This object returns an ApiResponse with hasMorePages set to true if we request a page
lower than page five. If we request page 5 or up, we get a response that has its hasMorePages
property set to false.
The object that fetches data is called RecursiveFetcher and I’m going to work off the
following skeleton implementation:
struct RecursiveFetcher {
let network: NetworkObject
}
}
All of my work will happen in loadAllPages(). When this method is called, I will kick off a
page load. Then when I get a response from this page load, I will inspect the response to see if
there are any more pages and then load another page. This is very similar to what you saw in
the previous section, except instead of having the user ask for more pages the code should
automatically “ask” for the next page when the previous page is loaded and there are more
pages to load.
The code should also collect all responses from all page loads, so we can emit a single array
of items (in this case [String]) to subscribers. To do this, we’ll use Combine’s reduce
operator.
When you apply reduce to a publisher in Combine, all events emitted by that publisher are
collected and reduced into a single value. Similar to how reduce works on Array in Swift.
When the upstream publisher completes, reduce will emit the single value that was reduced
from all values emitted by the upstream publisher.
Let’s implement loadAllPages to see how we can automatically trigger new page loads,
and learn more about reduce. Examine the code and try to figure out what it does before
skipping to the explanation:
return pageIndexPublisher
.flatMap({ pageIndex in
return network.loadPage(pageIndex)
})
.handleEvents(receiveOutput: { response in
if response.hasMorePages {
pageIndexPublisher.value += 1
} else {
pageIndexPublisher.send(completion: .finished)
}
})
.reduce([String](), { collectedStrings, response in
return response.items + collectedStrings
})
.eraseToAnyPublisher()
}
Were you able to figure out what this code does exactly? It can be really confusing at first but
I’m sure you got pretty far.
In the previous section, I used a PassthroughSubject to publish load requests that were
represented by a Void value. This time I’m using a CurrentValueSubject that has the
struct Token {
let isValid: Bool
}
class Authenticator {
private var currentToken = Token(isValid: false)
struct UserApi {
let authenticator: Authenticator
}
}
The Token object is a simple object that I defined to easily fake an expired token. The Au-
thenticator object is responsible for providing tokens and refreshing them. The UserApi
is the object that will make authenticated network requests.
The goal here is to obtain an inital token and make a network request. If the request comes
back with a 403 status code this means that the token we received initially should be refreshed
and another attempt at performing the network request should be made.
While the use case is vastly different from the previous section, the pattern is remarkably
similar.
In both cases we want to call a method that returns a publisher. When we subscribe to that
publisher, a network call should be kicked off. Depending on the response we should kick off
another network call or emit a response.
The only difference is that we don’t want to reduce anything this time. We just want to emit a
single value, as long as the network request we kicked off succeeded. If the request failed,
the faulty response should be hidden from subscribers so the token can be refresh and a new
request is made.
Since the full pipeline for this feature is rather long and complex, I will go through it bit by bit un-
til the pipeline is finished. First, we’ll need a subject that drives the pipeline by emitting an ini-
tial token. The CurrentValueSubject created by Authenticator.tokenSubject
is a perfect fit for this:
return tokenSubject
.eraseToAnyPublisher()
}
This code doesn’t quite compile but we’ll get there eventually.
Just like before, we’ll want to flatMap over the CurrentValueSubject to kick off a net-
work call whenever we receive a token:
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
.eraseToAnyPublisher()
}
Here’s where it gets interesting. If the result of the data task that’s created in this flatMap has
a 403 status code, we do not want to forward this value to subscribers. Instead, we want to
pretend we never received this value and kick off a token refresh and subsequently retry the
network request.
If the status code is anything other than 403, the data should be extracted from the URLSes-
sion.DataTaskPublisher.Output and forwarded down the pipeline so it’s received
by subscribers.
We can achieve this by using a flatMap and two special publishers. The first is Just. You
already know this one. It emits a single value and completes. The other is Empty. This
publisher emits no values and completes immediately.
To use these publishers, I need to flatMap over the data task publisher. This allows me to
inspect the data task publisher’s result and return a Just or Empty publisher depending
on the status code. While we’re at it, we can also kick off a token refresh if the status code is
403:
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
self.authenticator.refreshToken(using: tokenSubject)
return Empty().eraseToAnyPublisher()
}
return Just(result.data)
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
})
.eraseToAnyPublisher()
})
.eraseToAnyPublisher()
}
The new flatMap on its own is nothing special. We take the output from the data task and
replace that output with an AnyPublisher<Data, Never>. If the status code is 403, I
call self.authenticator.refreshToken(using: tokenSubject) to kick off a
token refresh and pass the tokenSubject to the authenticator so it can send a new
token over the tokenSubject which will kick off the initial network request again. I also
return an Empty publisher that must be erased to AnyPublisher. This publisher completes
immediately without emitting values. In other words, it pretends we never received any values
from the data task publisher and completes.
If the status code is good, we return a Just publisher that emits the extracted Data and
completes. We need to set its failure type to URLError to make it compatible with the rest
of the pipeline.
At this point, you have already implemented a token refresh flow but there’s one thing that
bothers me. The initial and subsequent requests are driven by a CurrentValueSubject
that’s obtained from Authenticator which is great. Every time tokenSubject emits a
new token we perform the network request so recursion is handled automatically, just like it
was in the previous section.
However, the tokenSubject never completes which means that the publisher returned by
getProfile() also never completes. Let’s fix that by making one more change to getPro-
file():
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
self.authenticator.refreshToken(using: tokenSubject)
return Empty().eraseToAnyPublisher()
}
return Just(result.data)
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
})
.eraseToAnyPublisher()
})
.handleEvents(receiveOutput: { _ in
tokenSubject.send(completion: .finished)
})
.eraseToAnyPublisher()
}
When the outermost flatMap emits a value I want to complete the tokenSubject imme-
diately. Since any faulty values are replaced with Empty, data tasks created in the flatMap
that’s applied to tokenSubject will only ever emit a value when the status code we received
is good, and we replace the data task publisher with a Just publisher.
And that’s all there is to it!
In Summary
I saved this chapter until the end of the book because quite frankly, these kinds of pipelines
can be mind-boggling if you try to tackle and understand them too soon in your Combine
journey. In this chapter, you have seen three fairly advanced uses of Combine that all followed
very similar patterns.
Once you understand patterns like the once I’ve shown in this chapter, you’ll find that a lot of
seemingly complex uses of Functional Reactive Programming actually follow the same sets of
rules and patterns. But it takes practice to recognize and understand these patterns.
In this chapter, I have hopefully helped you gain a deeper understanding of how you can model
actions, requests or even access tokens in a way that allows you to drive entire pipelines by
making good use of Combine’s Subjects, flatMap and other operators that you saw in this
chapter.
In the next and final chapter of this book, I won’t be teaching you anything new. I think you’re
ready to go out and explore Combine in the real world. You have all the knowledge you might
need, you understand all of the terminology and you’ve seen several examples of how you
might integrate Combine in a real app. I hope you’ve enjoyed reading this book as much as
I’ve enjoyed writing it, and I hope I’ve been able to translate everything I’ve learned about
Combine into a format that’s useful and understandable for you.
Thanks for reading this book and trusting me to teach you Combine!