Uploaded by Sajal Gupta

Practical Combine iOS Donny Wals

advertisement
Contents
Practical Combine: An introduction to Combine with real examples
6
Chapter overview
Chapter 1 - Introducing Functional Reactive Programming . . . . . . . . . . . . .
Chapter 2 - Exploring publishers and subscribers . . . . . . . . . . . . . . . . . .
Chapter 3 - Transforming publishers . . . . . . . . . . . . . . . . . . . . . . . . .
Chapter 4 - Updating the User Interface . . . . . . . . . . . . . . . . . . . . . . .
Chapter 5 - Using Combine to respond to user input . . . . . . . . . . . . . . . .
Chapter 6 - Using Combine for networking . . . . . . . . . . . . . . . . . . . . .
Chapter 7 - Wrapping existing asynchronous processes with Futures in Combine .
Chapter 8 - Understanding Combine’s Schedulers . . . . . . . . . . . . . . . . .
Chapter 9 - Building your own Publishers, Subscribers, and Subscriptions . . . .
Chapter 10 - Debugging your Combine code . . . . . . . . . . . . . . . . . . . . .
Chapter 11 - Testing code that uses Combine . . . . . . . . . . . . . . . . . . . .
Chapter 12 - Driving publishers and flows with subjects . . . . . . . . . . . . . .
Chapter 13 - Where to go from here . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
8
8
8
8
9
9
10
10
11
11
11
12
12
12
.
.
.
.
13
13
15
18
19
.
.
.
.
20
20
23
26
28
Transforming publishers
Applying common transformations to a publisher . . . . . . . . . . . . . . . . . .
Understanding the differences between map, flatMap and compactMap . . . . . .
Using compactMap in Combine . . . . . . . . . . . . . . . . . . . . . . . . .
30
30
35
35
Introducing Functional Reactive Programming
Understanding Functional Reactive Programming
Enhancing readability with FRP . . . . . . . . . .
Comparing Combine to RxSwift . . . . . . . . . .
In Summary . . . . . . . . . . . . . . . . . . . . .
Exploring Publishers and Subscribers
Creating your first Publisher . . . . . .
Subscribing to a Publisher’s stream . .
Understanding the lifecycle of a stream
In Summary . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Practical Combine
Using flatMap in Combine . . . . . . . . . . . . . . . . . . . . . . . .
Limiting the number of active publishers that are produced by flatMap
Applying operators that might fail . . . . . . . . . . . . . . . . . . . . . . .
Defining custom operators . . . . . . . . . . . . . . . . . . . . . . . . . . .
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Updating the User Interface
Creating publishers for your models and data . . . . . . . . . . . . . . . . . . . .
Using a PassthroughSubject to send a stream of values . . . . . . . . . . . .
Using a CurrentValueSubject to represent a stateful stream of values . . . . .
Wrapping properties with the @Published property wrapper to turn them into
publishers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Choosing the appropriate mechanism to publish information . . . . . . . . .
Directly assigning the output of a publisher with assign(to:on:) . . . . . . . . . . .
Using Combine to drive Collection Views . . . . . . . . . . . . . . . . . . . . . . .
Using Combine to update a collection view’s data source . . . . . . . . . . .
Driving collection view cells with Combine . . . . . . . . . . . . . . . . . . .
Assigning the output of a publisher to an @Published property with assign(to:)
Creating a simple theming system with Combine . . . . . . . . . . . . . . . . . . .
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Using Combine to respond to user input
Updating the UI based on user input . . . . . . . . . . . .
Limiting the frequency of user input . . . . . . . . . . . . .
Combining multiple user inputs into a single publisher . .
Combining publishers with Publishers.Zip . . . . . .
Combining publishers with Publishers.Merge . . . . .
Combining publishers with Publishers.CombineLatest
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . .
Using Combine for networking
Creating a simple networking layer in Combine .
Handling JSON responses from a network call .
User-friendly networking with Combine . . . . .
Building a complex chain of network calls . . . .
Donny Wals
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
37
42
45
46
49
51
52
52
55
58
60
60
64
65
69
72
76
81
.
.
.
.
.
.
.
82
82
87
97
99
104
106
108
.
.
.
.
110
110
115
123
126
3
Practical Combine
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Wrapping existing asynchronous processes with Futures in Combine
Understanding how Futures work . . . . . . . . . . . . . . . . . . .
Using Combine to ask for push permissions . . . . . . . . . . . . . .
Using Futures to fetch data from Core Data . . . . . . . . . . . . . .
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
131
132
132
141
145
147
Understanding Combine’s Schedulers
148
Exploring the Scheduler protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Understanding receive(on:) and subscribe(on:) . . . . . . . . . . . . . . . . . . . . 150
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Building your own Publishers, Subscribers, and Subscriptions
Understanding what happens when you subscribe to a publisher . . . . . . .
Implementing a custom Subscriber . . . . . . . . . . . . . . . . . . . .
Understanding backpressure . . . . . . . . . . . . . . . . . . . . . . .
Understanding how you can create a custom publisher and subscription
Extending UIControl with a custom publisher . . . . . . . . . . . . . . . . . .
In Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Debugging your Combine code
Using print to debug your code . . . . . . . . . . . . . .
Using Timelane to understand and debug your code . . .
Preparing to use Timelane . . . . . . . . . . . . . .
Using Timelane to debug a simple sequence . . . .
Using Timelane to debug a sequence of publishers .
In Summary . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Testing code that uses Combine
Writing tests for code that uses Combine . . . . . . . . . . . . .
Optimizing Combine code for testability . . . . . . . . . . . . .
Architecting code that uses a NotificationCenter publisher
Architecting code that requires a networking layer . . . . .
Creating helpers to make testing Combine code easier . . . . . .
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Donny Wals
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
158
159
161
164
166
170
174
.
.
.
.
.
.
176
176
177
178
181
189
192
.
.
.
.
.
.
193
194
201
202
206
216
223
4
Practical Combine
Driving publishers and flows with subjects
Retrieving partial data in Combine . . . . . . . . . . . . .
Recursively loading data with subjects and flatMap . . .
Automatically retrying a network call after a token refresh
In Summary . . . . . . . . . . . . . . . . . . . . . . . . .
Where to go from here?
Donny Wals
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
224
224
229
233
239
240
5
Practical Combine
Practical Combine: An introduction to
Combine with real examples
At WWDC 2019, Apple announced a framework called Combine. It’s Apple’s take on a familiar
concept that’s been popularized by RxSwift over the past couple of years, and it’s one of the
driving forces behind the new SwiftUI framework. Getting started with new technology like
this can be complicated and confusing, especially since Apple’s documentation isn’t always
up to snuff when it comes to being a learning resource.
To help bridge the gap between your existing codebase and learning functional reactive
programming with Combine I decided I wanted to write this book. And not just to write a book
that covers the framework on a theoretical level. Instead, I wanted to write a book that uses
examples that are based on real-world usage of functional reactive programming. I learn best
when I can see how new concepts can be applied to the code I write every day. And I know
many other folks like learning the same way.
Throughout this book, I will start you off with some theory. Without it, I don’t think any of the
examples would make sense and there would be a ton of friction in making sense of the code
I’d show you. But once the theory is out of the way, we’re off to the races. You will learn how
you can use Combine to drive your UI, how you can use it to respond to user input, how you
can integrate it with networking and much, much more. In the final chapter, I will show you
how you can write custom Combine publishers to extend the framework if needed to make it
fit your requirements.
I hope you will enjoy the journey I’m about to take you on, and that you will come out with
a good idea of how Combine can help you bring functional reactive programming into your
codebase.
This book would not have been possible without folks from the community who have been
asking me questions, and telling me what they want to learn. So if you’ve ever reached out
to me with a question or comment, I want to thank you for that. Your feedback helped me
write this book. I also want to thank all of my friends from the iOS community that reviewed
this book, and those that have encouraged me to write it. In no particular order, I want to
mention a couple of these folks. Nic Laughter for always being so positive and supportive.
Daniel Steinberg for encouraging me to continue doing what I love, and helping me to get
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
Chapter 1 - Introducing Functional
Reactive Programming
Learn the basic building blocks of Combine and Functional Reactive Programming (FRP). I
explain what FRP is, and how it can help you write better and cleaner code. You’ll immediately
see little bits and pieces of Combine code to help you see how FRP enhances code readability
in certain cases. You’ll also learn about some of the very superficial differences between
RxSwift and Combine. This will help you understand where Combine fits in the FRP ecosystem,
and it will help you choose between the two frameworks if your app is for iOS 13 and above.
Chapter 2 - Exploring publishers and
subscribers
Publishers are the bread and butter of the Combine framework. Without publishers, we’d
have nothing to subscribe to. In this chapter you will learn how publishers work, how you can
subscribe to the values they publish, and more importantly, you’ll learn what all of this means.
After going through a basic example, you will learn about the lifecycle of a subscription stream,
and how subscribing to a publisher works exactly.
Chapter 3 - Transforming publishers
In most cases, the values that are published by a publisher need to be modified before they
can be used by a subscriber. In this chapter, I demonstrate the usage of several of Combine’s
built-in operators like map, flatMap and compactMap. We’ll also cover some more advanced
operators like replaceNil and setFailureType. You will also learn about operators
that might throw an error to end the publisher stream, like tryMap, tryCompactMap and
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.
Chapter 4 - Updating the User Interface
In this fourth chapter, we’re finally going to get truly practical. You will learn how you can
use Combine to update the value of a label with a new subscriber called assign(to:on:).
You will also learn how Combine can be used in an MVVM-like architecture, and how it can
be used to drive a collection view’s diffable data source without implementing any specific
architecture. In addition to showing you how to update a collection view data source, I also
demonstrate how you can use Combine to download images that need to be displayed in a
collection view cell. And to top it all off, you’ll learn how to build a Combine-driven theme
manager that can be used to allow a user to switch your app between dark- and light mode
separate from the system setting.
Chapter 5 - Using Combine to respond to
user input
In this chapter, you’ll learn how to implement several simple bindings between UI components
and a model. Once you understand how this works in UIKit, I’ll demonstrate how the same
principle is implemented in SwiftUI because a lot of this functionality comes for free in SwiftUI.
You’ll also learn how you can restrict the processing of user input through a feature called
debouncing. This is especially useful if you’re building a feature where user input results in
expensive processing like networking for example. You will also learn about the different
mechanisms that Combine offers to merge and combine the output from multiple publishers
into a single publisher. By the end of this chapter, you should have a very good understanding
of how publishers in Combine can be updated, and how they can be used to integrate a model
and user interface element with Combine.
Donny Wals
9
Practical Combine
Chapter 6 - Using Combine for
networking
Throughout the preceding chapters, I often use networking as an example of a Combine
publisher. The usage of networking examples so far has been very basic though. In this
chapter, you will gain a more advanced understanding of networking in Combine, and how
Combine can help you support features like low data mode on iOS, or how you can implement
graceful error handling. In addition to networking, you will learn about some interesting new
operators like tryCatch and switchToLatest. You’ll implement a pretty complicated
token refresh flow with these new operators, and a couple of operators you’re familiar with
at this point. This chapter doesn’t stop there. I will also show you how you can orchestrate
several network calls at once, and merge their outputs into a single publisher. By the end of
this chapter, you will have all the knowledge needed to implement a robust networking layer
with Combine.
Chapter 7 - Wrapping existing
asynchronous processes with Futures in
Combine
Not all of your asynchronous code is easy to integrate with Combine. In this chapter, you will
learn about a very special publisher called a Future. This publisher doesn’t quite play by the
same rules as other publishers, but it’s a very convenient publisher that allows us to kick off
asynchronous work, and broadcast the result of this work to its subscribers. You will learn how
to implement a simple Future based interface on top of UNUserNotificationCenter,
and I will show you how to wrap code in a Future.
Donny Wals
10
Practical Combine
Chapter 8 - Understanding Combine’s
Schedulers
In Combine, a lot of threading and dispatch queue related logic is hidden from you, and that’s
extremely convenient. However, there are times where you might need to do some manual
work to make sure a certain publisher sends its events on the main queue, or you might want
to make sure that it doesn’t run code on the main queue. Combine abstracts dispatch queues
behind schedulers. Learn how schedulers work in this chapter, and find out how they impact
the code you write in your apps.
Chapter 9 - Building your own Publishers,
Subscribers, and Subscriptions
This chapter comes with a huge warning. Nothing in this chapter should be used in production.
However, we will conceptually pull apart what happens when you subscribe to a publisher
using sink by reimplementing the functionality. Apple does not recommend that users of
Combine do this, and it’s for a good reason. The fine details of how subscriptions work are
hidden from us as users of the framework. Regardless, I like to re-implement functionality in
an attempt to better understand how it might work which can lead to interesting insights.
Chapter 10 - Debugging your Combine
code
Debugging is hard, especially when you’re dealing with asynchronous code. In this chapter,
you will learn more about Combine’s print operator and you will learn about a community
tool called Timelane that can be used to visualize the lifecycle and emitted values of your
Combine subscriptions.
Donny Wals
11
Practical Combine
Chapter 11 - Testing code that uses
Combine
In this chapter, you will learn how you can use XCTest to write unit tests for your Combine
code. You will learn things that you should, and shouldn’t do when testing Combine code.
I will also show you how you can optimize your code for testability and I will show you two
convenient helpers that can help you improve and clean up your test suite.
Chapter 12 - Driving publishers and flows
with subjects
Learn how you can build really powerful and complex features using Combine’s
PassthroughSubject, CurrentValueSubject and some clever operators. You
will learn how to deal with a paginated API in Combine, how you can recursively load data
and I will show you an alternative approach to retrying network calls after a token refresh.
Chapter 13 - Where to go from here
While this book should prepare you for a lot of Combine use-cases, there’s also a lot that I
don’t cover. In this chapter, I aim to give you some insights into the features I didn’t cover, and
resources you might want to pick up to expand your Combine knowledge and expertise.
Donny Wals
12
Practical Combine
Introducing Functional Reactive
Programming
Combine is a so-called Functional Reactive Programming framework. This means that it has
something to do with functional programming, and reacting to an ever-changing environment.
Throughout this book, you will learn everything you need to know to adopt Combine, and
Functional Reactive Programming, in your projects. As you might expect, I will take a very
practical approach to teach you Combine to make sure you’re up and running with this
powerful framework as soon as possible.
To make sure that you understand what Combine is, and what it should be used for, I want to
make sure that you have a solid foundation first. This means that we’ll need to chew through
some less practical but fundamentally important chapters first. In this first chapter of Practical
Combine you will learn about the following topics:
• Understanding Functional Reactive Programming (FRP)
• Enhancing readability with FRP
• Comparing Combine to RxSwift
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.
Understanding Functional Reactive
Programming
As I mentioned, Combine is a Functional Reactive Programming framework. Through this
chapter and the rest of the book, I will refer to Functional Reactive Programming as FRP which
is a common abbreviation used in the community. FRP is built upon principles that come from
Functional Programming. This means that FRP enables us to write code that can be composed
using many small functions that operate only on their inputs without changing anything that’s
outside of the function itself. An example of this is the well-known map function in Swift:
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.
Enhancing readability with FRP
When you’re just starting out with FRP, you might wonder how it’s any better than not using
FRP. As you will find out throughout this book, FRP can increase code readability and it reduces
code complexity dramatically. FRP’s most significant drawback is that it tends to have quite a
steep learning curve. Once the principles click, and you understand how FRP and Combine
work, it starts to become very clear how FRP enhances readability.
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:
let myUrl = URL(string: "https://www.donnywals.com")!
func requestData(_ completion: @escaping (Result<Data, Error>) ->
,→
Void) {
URLSession.shared.dataTask(with: myUrl) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
preconditionFailure("If there is no error, data should be
,→
present...")
}
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:
let myUrl = URL(string: "https://www.donnywals.com")!
func requestData() -> AnyPublisher<Data, URLError> {
URLSession.shared.dataTaskPublisher(for: myUrl)
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
Comparing Combine to RxSwift
If you have experience with FRP, you might have skipped the previous two sections to get to
this section right away. You’d probably be wondering, how is Combine different from RxSwift?
And why would I switch? Or if you’re just learning about FRP, you might be wondering which
one of the two frameworks you should choose based on their similarities and differences.
There are several FRP frameworks available for iOS but and I chose to look at RxSwift and
Combine only. A framework that I won’t look at but wanted to mention anyway just because
of its user base is ReactiveCocoa. The ReactiveCocoa framework has many users, maybe even
more than RxSwift has based on their GitHub stars at the time of writing, but its focus on UI
elements makes it too different from RxSwift and Combine for me to consider it in this brief
comparison.
RxSwift is a framework that implements the cross-platform ReactiveX API. ReactiveX is a
standard for reactive programming that is adopted on many platforms. There are flavors of
Javascript, Java, and more available. If you’re working in a cross-platform team, this means
that code from one platform can sometimes be pretty much copied and pasted to other
projects. That on its own is a huge benefit. On top of that RxSwift is open source which means
that you have insight into its development, and you can even contribute bug fixes if you’d
like.
That said, RxSwift is a third-party dependency. And just like any other third-party dependency,
you have to trust the project’s maintainers to ship bug-free, safe code on a timeline that fits
yours. And on top of that, RxSwift’s maintainers can decide to stop maintaining the project
whenever they please. This, of course, is unlikely but not impossible.
With Combine, you buy into Apple’s ecosystem. This means that you’re at their mercy for bug
fixes to arrive in new iOS versions, and the framework’s behavior might change between iOS
versions. On the flip side, you can be pretty sure that Apple will keep Combine around for a
long time, even if it’s just for compatibility reasons. And more importantly, you don’t need to
import any external code into your codebase.
Overall, I think Combine is probably a better choice for the sole reason that it’s Apple’s FRP
framework. It’s tightly integrated with some of Apple’s existing APIs and it’s the driving force
behind SwiftUI. So if you have plans to use SwiftUI or any of the integrated Combine features,
it makes more sense to use Combine in my opinion because you would otherwise mix different
Donny Wals
18
Practical Combine
FRP frameworks in one project.
In the end, it’s your choice but since you’re reading this book, I think you may have made
up your mind. And if you haven’t, this book should provide you with enough examples and
information to help you make a decision.
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 higherorder 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 complexity 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 Combine. 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
Exploring Publishers and Subscribers
In the previous chapter, you learned about what Combine is, and the principles it is built upon.
I think it’s important to understand these principles, or at least be aware of them before we
move on to using Combine. Because you know what FRP is, and what its characteristics are, I
hope that the content in this chapter and all chapters that come after it are a lot clearer than
they would be if I had started with this chapter.
In this chapter, you will learn about some of Combine’s fundamental building blocks; publishers and subscribers. You will learn about the following topics:
• Creating your first Publisher
• Subscribing to a publisher’s stream
• Understanding the lifecycle of a stream
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.
Creating your first Publisher
In Combine, everything starts with a publisher. Without a publisher, there is nothing to
subscribe to and nothing to receive values from. So whenever you want to do something
that involves Combine, it’s a given that there will be a publisher involved in some way. In the
previous chapter I’ve already shown you some Combine code to illustrate some of the points I
was making, but let’s rewind a bit to create and examine a very basic publisher:
let publisher = [1, 2, 3].publisher
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 publishers. 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 signature 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 Publishers.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
Figure 1: A marble diagram for a publisher that emits three values
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:
let myUrl = URL(string: "https://www.donnywals.com")!
let publisher = URLSession.shared.dataTaskPublisher(for: myUrl)
The publisher created by the preceding code is a URLSession.DataTaskPublisher.
This publisher conforms to the Publisher protocol just like any other publisher. Its Output
is (data: Data, response: URLResponse) and its Failure is URLError. This
means that we’ll receive a tuple of (data: Data, response: URLResponse) when
the request succeeds, and an error of type URLError if the request fails. Since a request only
executes once, the publisher completes as soon as the request has either succeeded or failed.
The following image shows a marble diagram that resembles a successful request.
Donny Wals
22
Practical Combine
Figure 2: A marble diagram that represents a network request
Apple has also added a specialized publisher for NotificationCenter:
let publisher = NotificationCenter.default.publisher(for:
,→
UIResponder.keyboardWillShowNotification)
The publisher created in this code is a NotificationCenter.Publisher that has Notification as its Output and Never as its Failure. What’s interesting about this publisher when compared to URLSession.DataTaskPublisher is that it never actually
completes. As long as an app is alive, the NotificationCenter.Publisher can emit values for the Notification that we are subscribed to. Because the publisher never completes,
it also never errors which is why Never is the appropriate error for this publisher.
I will show you how to use publishers in the context of a real app in later chapters. So far, I
have shown you how to create instances of publishers. But only instantiating publishers isn’t
very useful. Let’s see how you can receive, and react to values that are emitted by a Combine
publisher.
Subscribing to a Publisher’s stream
Publishers in Combine emit values and we call this a stream. Objects that receive these values
are called subscribers. A subscriber, as the name suggests, subscribes to the output of a
publisher. Combine provides us with two very convenient general-purpose subscribers out of
the box. I will show them one by one, starting with the sink subscriber:
Donny Wals
23
Practical Combine
[1, 2, 3].publisher.sink(receiveCompletion: { completion in
print("publisher completed: \(completion)")
}, receiveValue: { value in
print("received a value: \(value)")
})
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 because 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 Subscribers.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:
[1, 2, 3].publisher.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("finished succesfully")
case .failure(let error):
print(error)
}
}, receiveValue: { value in
print("received a value: \(value)")
})
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:
[1, 2, 3].publisher.sink(receiveValue: { value in
print("received a value: \(value)")
})
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:
var user = User()
["test@email.com"].publisher.assign(to: \.email, on: user)
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
Understanding the lifecycle of a stream
In Combine, a publisher that doesn’t have a subscriber will not emit any values. This means
that publishers won’t perform any work unless they have a subscriber to send the result of their
work to. This is a powerful feature of Combine, and it’s one of Combine’s core principles.
In Combine, publishers only perform work, and they only publish values if there is a subscriber
listening and if the subscriber is willing to receive values. Typically, you will not need to worry
about this. Both sink and assign create subscribers that are always willing to receive
values so the publishers they subscribe to will immediately perform work when needed, and
they will emit values as often as needed.
The principle of making the emission of values subscriber-driven is called backpressure management. If you want to learn more about this, you can skip ahead to Chapter 8 - Understanding Combine’s Schedulers. In that chapter you will learn about the finer details of
publishers and subscribers, and how you implement backpressure management.
For now, it’s important that in the lifecycle of a stream, publishers will only begin emitting
values after a subscriber is attached to them.
Both the sink and assign methods return a very important object that I have not mentioned
yet. This object is an AnyCancellable and it’s a crucial object in the lifecycle of a subscriber.
When an AnyCancellable is deallocated, the subscription that is associated with this
object is torn down along with it. Let me demonstrate with an example:
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 listenToNotifications 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))
}
The subscription property would normally be an instance variable on a class or struct.
This means that the AnyCancellable can stick around for as long as the containing instance
is around. While assigning to a property is fine for a single subscription, it might be less than
ideal if you have multiple subscriptions that you want to hold on to. Luckily, Combine has a
second way of storing AnyCancellable instances:
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 parameter 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 publisher 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:
•
•
•
•
Applying custom transformations to a publisher
Understanding the differences between map, flatMap, and compactMap
Applying transformations that might fail
Defining custom transformations
You won’t learn everything about all the different operators that Combine has to offer. Instead, 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.
Applying common transformations to a
publisher
Normally, when you subscribe to a publisher you receive its values immediately, as is. Consider
the following example:
let myLabel = UILabel()
[1, 2, 3].publisher
.sink(receiveValue: { int in
Donny Wals
30
Practical Combine
myLabel.text = "Current value: \(int)"
})
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.
let myLabel = UILabel()
[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
Figure 3: A marble diagram that applies a map operator
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>
This is quite convenient, we were able to turn a Publishers.Sequence<[Int],
Never> into a Publishers.Sequence<[String], Never> without any trouble.
Unfortunately, this isn’t always how map works. Let’s look at a brief example of a network
request in Combine:
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 documentation. If you read this page, you’ll find that towards the end of the overview it says the
following:
For example, the map(_:) operator returns an instance of Publishers.Map.
This is clearly what we saw when mapping the data task publisher. But it’s not what happened 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 Publishers.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 documentation for Publishers. There are several interesting publishers like Publishers.Scan,
Donny Wals
33
Practical Combine
Publishers.Drop, Publisher.Merge or even Publishers.Merge2, Publishers.Merge3, Publishers.Merge4 and so forth.
Each of these publishers takes a publisher as input and transforms its output so it can be used
as an output for that specific publisher. Sometimes publishers take a single publisher and
they modify or ignore values from that publisher. Other times, a publisher might take several
publishers, merging them all into a publisher that publishes values from all of the merged
publishers. I will show you some of these publishers in action later in the chapter, and you
will find plenty more in this book. A function like map that wraps a publisher into another
publisher is called an operator, and I will try to refer to them as operators where possible.
Alongside publishers and subscribers, operators are what makes Combine such a powerful framework. By chaining publishers and operators together, you can turn complex asynchronous code into a declarative list of operations that is much simpler to reason about than
its procedural counterpart. When you look at the following code, I want you to focus on what
you think it does. Don’t worry if you don’t understand how, or why, this code works. We’ll get
to that soon:
let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for:
someURL)
,→
.retry(1)
.map({ $0.data })
.decode(type: User.self, decoder: JSONDecoder())
.map({ $0.name })
.replaceError(with: "unknown")
.assign(to: \.text, on: userNameLabel)
The preceding code goes through the following steps.
•
•
•
•
•
•
•
Make a request to someURL.
Retry the request once if it fails.
Grab data from the network response.
Decode the data into a model of type User.
Grab the user’s name.
Replace any errors that we’ve encountered along the way with the string “unknown”.
Set the obtained string to a label’s text.
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 builtin 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.
Understanding the differences between
map, flatMap and compactMap
In the previous sections, I have shown you a couple of examples that demonstrated the
usage of map in Combine. If you’re familiar with Swift, you’ll know that in addition to map, it
has compactMap and flatMap. Both of these additional mapping functions are present in
Combine and they work in the same ways that their imperative counterparts that you know
from Swift’s Sequence and Collection types. In this section, I will show you how and
when it’s appropriate to use each of these operators and how they work.
Using compactMap in Combine
When you call compactMap on a Collection in Swift it works a lot like a normal map,
except all nil results are filtered. Let’s look at an example:
let result = ["one", "2", "three", "4", "5"].compactMap({ Int($0) })
print(result) // [2, 4, 5]
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:
["one", "2", "three", "4", "5"].publisher
.compactMap({ Int($0) })
.sink(receiveValue: { int in
print(int)
})
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:
["one", "2", "three", "4", "5"].publisher
.map({ Int($0) })
.replaceNil(with: 0)
.sink(receiveValue: { int in
print(int)
})
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
["one", "2", "three", "4", "5"].publisher
.map({ Int($0) })
.replaceNil(with: 0)
.compactMap({ $0 })
.sink(receiveValue: { int in
print(int) // int is now a non-optional Int
})
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.
Using flatMap in Combine
The last of the three mapping flavors that I wanted to show you is flatMap. When I was just
becoming familiar with concepts like map and flatMap in Functional Programming, I was
somewhat confused about this curious function. And let’s be honest, the short description for
flatMap in the documentation isn’t fantastic:
Returns an array containing the concatenated results of calling the given transformation
with each element of this sequence.
Luckily, we can find a somewhat better explanation in the Discussion section of the documentation:
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:
let numbers = [1, 2, 3, 4]
let mapped = numbers.map { Array(repeating: $0, count: $0) }
// [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
let flatMapped = numbers.flatMap { Array(repeating: $0, count: $0) }
// [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
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:
var baseURL = URL(string: "https://www.donnywals.com")!
["/", "/the-blog", "/speaking", "/newsletter"].publisher
.map({ path in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
.sink(receiveCompletion: { completion in
print("Completed with: \(completion)")
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:
var baseURL = URL(string: "https://www.donnywals.com")!
["/", "/the-blog", "/speaking", "/newsletter"].publisher
.flatMap({ path -> URLSession.DataTaskPublisher in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
.sink(receiveCompletion: { completion in
print("Completed with: \(completion)")
}, receiveValue: { result in
print(result)
})
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:
let baseURL = URL(string: "https://www.donnywals.com")!
var cancellables = Set<AnyCancellable>()
["/", "/the-blog", "/speaking", "/newsletter"].publisher
.setFailureType(to: URLError.self) // This is only needed on iOS 13
.flatMap({ path -> URLSession.DataTaskPublisher in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
.sink(receiveCompletion: { completion in
print("Completed with: \(completion)")
}, receiveValue: { result in
print(result)
}).store(in: &cancellables)
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
Figure 4: A marble diagram that visualizes flatMap
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?
Limiting the number of active publishers that are produced
by flatMap
So far I have shown you how you can use flatMap in a pretty immediate fashion. We’d
receive values, change them into a new publisher and the nested publisher’s results were then
delivered to the sink. There are times when this isn’t what you want. Consider a scenario
where you make API requests based on something a user does. If the user would perform this
action quick enough, and our code doesn’t handle this appropriately, we could end up with
many simultaneous API calls being executed at the same time.
In the previous chapter I briefly mentioned backpressure, and I will continue to mention this
term throughout this book. I briefly explained that backpressure relates to how Combine
allows subscribers to communicate how many values they wish to receive from the publisher
they’re subscribed to. In other words, they can communicate how much input they want
to receive from their upstream publisher. As it turns out, publishers can also limit the input
they receive from their upstream publisher. The flatMap operator supports this through its
maxPublisher argument. I would like to highly recommend that you open up a Playground
in Xcode and run the following code:
[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:
receive subscription: ([1, 2, 3])
request unlimited
receive value: (1)
got: 1
got: 1
receive value: (2)
got: 2
got: 2
receive value: (3)
got: 3
got: 3
receive finished
I want to focus on the first two lines only:
receive subscription: ([1, 2, 3])
request unlimited
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 maxPublishers 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:
receive subscription: ([1, 2, 3])
request max: (1)
receive value: (1)
got: 1
got: 1
request max: (1)
receive value: (2)
got: 2
got: 2
request max: (1)
receive value: (3)
got: 3
got: 3
request max: (1)
receive finished
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.
Applying operators that might fail
All the operators you’ve used so far didn’t allow you to throw errors from the operator. This
means that every transformation you applied to the values that are emitted by a publisher has
to succeed. Often this is perfectly fine, it’s not common to have transformations that might
fail.
This doesn’t mean that your transformation can’t ever fail, or that it’s bad if they do. Many
of Combine’s built-in operators come with versions that are prefixed with the word try. For
example tryMap, tryCompactMap, and others. Operators with a try prefix work identical
to their regular counterparts with the only exception being that you can throw errors from the
try versions.
Let’s look at an example:
enum MyError: Error {
case outOfBounds
}
[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 setFailureType 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.
Defining custom operators
Combine has loads of built-in operators but there will be cases where the built-in operators
don’t match what you need. This typically happens if you have repetitive, or long code inside
of a single operator. When this happens, defining an operator of your own might help to make
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:
var baseURL = URL(string: "https://www.donnywals.com")!
["/", "/the-blog", "/speaking", "/newsletter"].publisher
.setFailureType(to: URLError.self) // This is only needed on iOS 13
.flatMap({ path -> URLSession.DataTaskPublisher in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
.sink(receiveCompletion: { completion in
print("Completed with: \(completion)")
}, receiveValue: { result in
print(result)
}).store(in: &cancellables)
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:
extension Publisher where Output == String, Failure == Never {
func toURLSessionDataTask(baseURL: URL) ->
,→
AnyPublisher<URLSession.DataTaskPublisher.Output, URLError> {
if #available(iOS 14, *) {
return self
.flatMap({ path -> URLSession.DataTaskPublisher in
let url = baseURL.appendingPathComponent(path)
return URLSession.shared.dataTaskPublisher(for: url)
})
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; eraseToAnyPublisher(). 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 applying flatMap is Publishers.FlatMap<P, Publishers.SetFailureType<Self,
URLError>>. This isn’t the most readable and useful return type. It’s also an implementation detail of our operator. By erasing this implementation detail, we can return AnyPublisher<URLSession.DataTaskPublisher.Output, URLError>. This tells
users of our custom operator everything they need to know. This custom operator can be
used as follows:
var baseURL = URL(string: "https://www.donnywals.com")!
Donny Wals
48
Practical Combine
["/", "/the-blog", "/speaking", "/newsletter"].publisher
.toURLSessionDataTask(baseURL: baseURL)
.sink(receiveCompletion: { completion in
print("Completed with: \(completion)")
}, receiveValue: { result in
print(result)
}).store(in: &cancellables)
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, replaceNil 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 modified 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 constraining 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
Updating the User Interface
In the first section of this book, I focussed on teaching you the fundamental basics that you
need to know and understand to use Combine effectively in your apps. In this chapter and the
chapters to come, you will see how Combine integrates with applications using patterns and
principles that you are likely to be familiar with. In this chapter, we’ll get started with one of
the most important aspects of almost every application, the user interface.
I know that SwiftUI is the new and modern way to build user interfaces on iOS 13 and onward, but I will focus on using Combine with UIKit rather than SwiftUI in this chapter. The
reason you’ll learn Combine with UIKit instead of SwiftUI is that SwiftUI is so tightly integrated
with Combine that it can be really difficult to understand where Combine ends and SwiftUI
begins.
By teaching you how to use Combine in a UIKit or UI framework agnostic environment I hope
to show you exactly what Combine does for your UI, and what it’s good at. If you don’t have
any UIKit experience and went all-in on SwiftUI, I’m sure that you will be able to follow along.
A lot of the code that you’ll encounter is abstracted in such a way that it only interfaces with
UIKit where needed. And most importantly, I will spare the complications of building full
apps. Instead, you’ll see small and self-contained examples that you can integrate with your
projects.
In this chapter, we’re going to get started with the following topics:
•
•
•
•
•
Creating publishers for your models and data.
Directly assigning the output of a publisher with assign(to:on:).
Using Combine to drive Collection Views.
Assigning the output of a publisher to an @Published property with assign(to:).
Creating a simple theming system with 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
Creating publishers for your models and
data
In the first couple of chapters of this book, I used a handful of publishers to explain the basics
of Combine to you. I made extensive use of the sequence publisher in particular because it’s
a nice way of demonstrating how Combine works with a stream of strings, integers or other
values. And while it’s nice for that purpose, it’s not particularly useful in a lot of real-world
scenarios.
In this section, I will show you how you can create publishers that wrap values from your
models so you can expose them to your user interface and other parts of your app. There are
three techniques that you’ll learn in this section:
• Using a PassthroughSubject to send a stream of values.
• Using a CurrentValueSubject to represent a stateful stream of values.
• Wrapping properties with the @Published property wrapper to turn them into publishers.
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.
Using a PassthroughSubject to send a stream of values
Combine’s PassthroughSubject is one of two Subject publishers in the framework. A
subject in Combine is a special kind of publisher that allows the developer, that’s you, to inject
values into its stream. In other words, a subject allows you to determine which values are
published, and more importantly, when. Combine subjects are especially useful when your
code is (partially) written in an imperative style. This is often the case for existing applications,
or for code that is close to the user interface. Subjects in Combine have a send(_:) method
that allows you to send values down the publisher’s stream of values. It’s also possible to use
send(completion:) to complete a stream of values if needed.
The purpose of a PassthroughSubject in Combine is to send a stream of values from
their origin, through the publisher, to its subscribers. The origin of the value stream is often
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 application 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:
var cancellables = Set<AnyCancellable>()
let notificationCenter = NotificationCenter.default
let notificationName = UIResponder.keyboardWillShowNotification
let publisher = notificationCenter.publisher(for: notificationName)
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 NotificationCenter.Publisher. However, it fits my description of a PassthroughSubject really well
because it displays the same behavior as I describe for PassthroughSubject. Let’s reimplement NotificationCenter.Publisher using a PassthroughSubject to see
just how similar they are.
Donny Wals
53
Practical Combine
var cancellables = Set<AnyCancellable>()
let notificationSubject = PassthroughSubject<Notification, Never>()
let notificationName = UIResponder.keyboardWillShowNotification
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(forName: notificationName, object: nil,
,→
queue: nil) { notification in
notificationSubject.send(notification)
}
notificationSubject
.sink(receiveValue: { notification in
print(notification)
}).store(in: &cancellables)
notificationCenter.post(Notification(name: notificationName))
This example subscribes to the NotificationCenter using the old addObserver(forName:object:
method to subscribe to a notification using a closure that is executed whenever a specific notification is dispatched by the NotificationCenter. The code also defines a
PassthroughSubject<Notification, Never>. This is the publisher that relays the
notifications generated by NotificationCenter to its subscribers.
Every time the notification that we subscribed to is posted by the notification center, the
line notificationSubject.send(notification) is executed. This sends the received
notification directly to the PassthroughSubject which will deliver it to its subscribers
immediately.
Note that the PassthroughSubject does not hold on to any of the values that it has
sent in the past. All it does is accept the value that you want to send, in this case, a notification, and that value is immediately sent to all subscribers and discarded afterward. If you
do need to have a sense of state for a property, like when you have a model with mutable
values, you need the second type of Subject publisher that’s provided by Combine, the
CurrentValueSubject.
Donny Wals
54
Practical Combine
Using a CurrentValueSubject to represent a stateful stream
of values
A CurrentValueSubject in Combine looks and feels very similar to a PassthroughSubject. This is because they both conform to the Subject protocol. However, don’t be
fooled by their similarities because they serve very different purposes. Consider the following
code, that you might have written or seen at some point in the past:
class Car {
var onBatteryChargeChanged: ((Double) -> Void)?
var kwhInBattery = 50.0 {
didSet {
onBatteryChargeChanged?(kwhInBattery)
}
}
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
let kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough
,→
charge in battery")
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:
let car = Car()
someLabel.text = "The car now has \(car.kwhInBattery)kwh in its
,→
battery"
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
func drive(kilometers: Double) {
let kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery.value, "Can't make trip, not
,→
enough charge in battery")
kwhInBattery.value -= kwhNeeded
}
}
let car = Car()
// don't forget to store the AnyCancellable if you're using this in a
real app
,→
car.kwhInBattery
.sink(receiveValue: { newCharge in
someLabel.text = "The car now has \(newCharge)kwh in its battery"
})
In this new version of Car, kwhInBattery is no longer Double. Instead, it’s a CurrentValueSubject 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 accessing 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 PassthroughSubject.
Notice that there is no explicit call to send(_:) in this example. When you change a CurrentValueSubject’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 CurrentValueSubject 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:
// Don't forget to store your cancellable when you're not running
this in a Playground!
,→
car.kwhInBattery
.sink(receiveValue: { newCharge in
someLabel.text = "The car now has \(newCharge)kWh in its
battery"
,→
})
car.drive(kilometers: 100) // label will now show that there's 36kwh
,→
remaining
As you’ll see throughout this chapter, CurrentValueSubject is a fantastic fit to expose a
model’s properties through a reactive interface. There is one more very special way to publish
property values in Combine by using the @Published property wrapper.
Donny Wals
57
Practical Combine
Wrapping properties with the @Published property
wrapper to turn them into publishers
Those familiar with SwiftUI have likely encountered the @Published property wrapper, and
you may have considered it to be a part of SwiftUI. In reality, @Published is a Combine
construct. You can use the @Published property wrapper for similar purposes as CurrentValueSubject with some minor differences. Before I cover these differences, I would
like to show you how to use @Published by refactoring the car example one last time:
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
let kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough
,→
charge in battery")
kwhInBattery -= kwhNeeded
}
}
let car = Car()
// don't forget to store the AnyCancellable if you're using this in a
real app
,→
car.$kwhInBattery
.sink(receiveValue: { newCharge in
someLabel.text = "The car now has \(newCharge)kwh in its battery"
})
When you use @Published, you can access and modify the value of kwhInBattery directly. 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 directly.
Donny Wals
58
Practical Combine
To subscribe to a @Published property’s changes, you need to use a $ prefix on the property 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 wrapper 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
The output of this code would be the following:
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.
Choosing the appropriate mechanism to publish
information
Which publishing mechanism is best for you depends on several factors. If you’re publishing a
stream of events without a concept of state, you’re probably looking for a PassthroughSubject. These are not commonly found on models, and they are better suited for publishing
a stream of user interactions, or as I’ve shown, an event stream like NotificationCenter
emits. If you have a model that’s defined as a struct, you can expose new data through a
CurrentValueSubject. This allows you to continuously send new values to subscribers
while keeping a concept of the current state, or value. And lastly, if you have a class and you
want to have similar behavior to a CurrentValueSubject except for not being to end
the stream of values if needed, the @Published property wrapper is likely to fit your needs
perfectly.
Throughout this book, I will use all three of these mechanisms so you can see how they can
be used, and what their usage looks like.
Directly assigning the output of a
publisher with assign(to:on:)
In the previous section, I’ve shown you the following code:
Donny Wals
60
Practical Combine
class Car {
@Published var kwhInBattery = 50.0
let kwhPerKilometer = 0.14
func drive(kilometers: Double) {
let kwhNeeded = kilometers * kwhPerKilometer
assert(kwhNeeded <= kwhInBattery, "Can't make trip, not enough
,→
charge in battery")
kwhInBattery -= kwhNeeded
}
}
let car = Car()
// don't forget to store the AnyCancellable if you're using this in a
real app
,→
car.$kwhInBattery
.sink(receiveValue: { newCharge in
someLabel.text = "The car now has \(newCharge)kwh in its battery"
})
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
mutating func drive(kilometers: Double) {
Donny Wals
61
Practical Combine
let kwhNeeded = kilometers * car.kwhPerKilometer
assert(kwhNeeded <= car.kwhInBattery, "Can't make trip, not
,→
enough charge in battery")
car.kwhInBattery -= kwhNeeded
}
}
class CarStatusViewController {
let label = UILabel()
let button = UIButton()
var viewModel: CarViewModel
var cancellables = Set<AnyCancellable>()
init(viewModel: CarViewModel) {
self.viewModel = viewModel
}
// setup code goes here
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 CarStatusViewController 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:
lazy var batterySubject: AnyPublisher<String?, Never> = {
return car.$kwhInBattery.map({ newCharge in
return "The car now has \(newCharge)kwh in its battery"
}).eraseToAnyPublisher()
}()
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 publisher 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 Subscriber, 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.
Using Combine to drive Collection Views
Tons of apps on the App Store make use of table views and collection views, so I think it’s
absolutely key to cover these components in the context of Combine. I will focus on collection
views in this section, and due to the similarities between table views and collection views,
I won’t cover table views separately. All principles in this section should apply to both. The
example code will not show you how to set up a collection view from scratch. I’m going to
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:
• Using Combine to update a collection view’s data source
• Driving collection view cells with Combine
Using Combine to update a collection view’s data source
There are several ways to drive collection view data sources with Combine. However, no
matter what kind of design you come up with, the basic principle will always be the same.
When the collection view’s underlying data changes, you want to update the collection view
so it displays this new data, which is achieved by applying a new snapshot to the diffable
data source. Because of this, I’m not going to focus on implementing an extremely clever
mechanism that tells you which sections or items have changed. That’s what the collection
view’s diffable data source is for! Instead, I will show you a simple yet effective setup that
will get you up and running with a reactive collection view in no-time. First, let’s explore how
data can be sent from a data provider to the view controller where it will be assigned to the
collection view:
struct CardModel: Hashable, Decodable {
let title: String
let subTitle: String
let imageName: String
}
class DataProvider {
let dataSubject = CurrentValueSubject<[CardModel], Never>([])
func fetch() {
let cards = (0..<20).map { i in
Donny Wals
65
Practical Combine
CardModel(title: "Title \(i)", subTitle: "Subtitle \(i)",
,→
imageName: "image_\(i)")
}
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 CurrentValueSubject 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 automatically 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 }
let jsonDecoder = JSONDecoder()
if let models = try? jsonDecoder.decode([CardModel].self,
,→
from: value.data) {
self.dataSubject.value += models
}
}).store(in: &cancellables)
}
}
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 URLSession.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:
override func viewDidLoad() {
dataProvider.fetch()
dataProvider.dataSubject
.sink(receiveValue: self.applySnapshot)
.store(in: &cancellables)
}
func applySnapshot(_ models: [CardModel]) {
var snapshot = NSDiffableDataSourceSnapshot<Int, CardModel>()
snapshot.appendSections([0])
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
Note that I call fetch() before subscribing to dataProvider.dataSubject. The reason I do this could be considered a cheat. For some reason, diffable data sources don’t like
being updated rapidly. If you would subscribe to dataProvider.dataSubject before
calling fetch(), you’d immediately get an empty array as a current value, and then as soon
as fetch() is called, you’d get a populated array which wouldn’t be picked up by the diffable data source. So to work around that in this example, I’ve placed fetch() before my
subscription. This is only a problem because my example here doesn’t make any network
calls. In an application that fetches data from the network, you wouldn’t see this problem
because it would take longer for data to become available and the data source would not be
updated as rapidly.
There is a very neat way to prevent publishers from emitting values too rapidly, you’ll learn
about that technique in the next chapter when I show you how to handle user input with
Combine.
By passing applySnapshot(_:) to the sink(receiveValue:) method, we make
sure that a new snapshot for the diffable data source is generated every time that new model
data is available. And when we call apply(_:) to activate the snapshot, all the complicated
diffing is done by the diffable data source. This makes our job a heck of a lot easier than you
may have expected.
What’s nice about this approach is that you wouldn’t have to change a thing if you’ want to
implement a paginated version of the DataProvider, and if you’d change to the version
where fetch() returned an AnyPublisher, all you’d need to do is subscribe to the result
of fetch() instead of the dataSubject.
Driving collection view cells with Combine
A slightly more complicated concept is using Combine to drive collection view cells. Of course,
it depends on how far you want to take Combine for your cells, and before you blindly use
Combine to update all of your cells, you have to wonder whether your cells contain data that
will change dynamically and over time. In my experience, the models that drive collection
view cells don’t change much with one huge exception. When your cells display images from
the web, there’s a very good chance that you will load those images asynchronously, and
your cell will need to update once the image is loaded. You can achieve functionality like this
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:
func fetchImage(named imageName: String) -> AnyPublisher<UIImage?,
,→
URLError> {
let url = URL(string: "https://imageserver.com/\(imageName)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map { result in
return UIImage(data: result.data)
}
.eraseToAnyPublisher()
}
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
class CardCell: UICollectionViewCell {
var cancellable: Cancellable?
let imageView = UIImageView()
// all other code is omitted for brevity
}
This example cell has an image and a cancellable property. Note that I used Cancellable and not AnyCancellable as its type. I like to write my code as flexible as
reasonably possible, and since AnyCancellable conforms to the Cancellable protocol, I figured this would be a good choice.
Now let’s see how we’d tie the DataFetcher to the CardCell and update its imageView:
let datasource = UICollectionViewDiffableDataSource<Int,
,→
CardModel>.init(collectionView: collectionView) { collectionView,
,→
indexPath, item in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
CardCell.identifier, for: indexPath) as! CardCell
,→
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.
Assigning the output of a publisher to an
@Published property with
assign(to:)
In iOS 14 Apple added a new way of subscribing to publishers in the form of assign(to:).
This subscriber was created to link the output of a publisher to a @Published property
without causing any retain cycles like assign(to:on:) does when assigning to a keypath
on self. Having the ability to set up a direct chain you like can with assign(to:) is
especially useful when you’re building an object that exposes a @Published property that
represents some kind of state that’s updated due to external factors. For example, every
time you call a function that uses a publisher to asynchronously perform some work and
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 }
let jsonDecoder = JSONDecoder()
if let models = try? jsonDecoder.decode([CardModel].self,
,→
from: value.data) {
self.dataSubject.value += models
}
}).store(in: &cancellables)
}
}
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 dataSubject 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
and a minor refactor to dataSubject:
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 ?? []
return currentModels + models
})
.replaceError(with: fetchedModels)
.assign(to: &$fetchedModels)
}
}
Notice that I renamed dataSubject to fetchedModel and made it a @Published property instead of a CurrentValueSubject. In fetchNextPage(), I still make the same
URL request, except instead of subscribing to the request’s result, I use a tryMap to grab the
network call’s result and attempt to convert it to JSON. If this succeeds, I return the currently
fetched models combined with the newly fetched models.
If the network call or JSON decoding fails, I replace the error that’s emitted with the current
value of fetchModels, effectively ignoring all errors. And on the last line I call assign(to:
&$fetchedModels). This will automatically assign the value that’s returned from my
tryMap (or replaceError if something goes wrong) to the fetchedModels property,
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
Creating a simple theming system with
Combine
In this section, I will show you how to create a simple theming system. We’ll implement a
central theme manager which has a publisher for the desired theme. Changing the theme will
immediately update the entire app. Because iOS 13 supports dark and light theming out of
the box through system settings, the setting I’m talking about in this section would be used to
override the system setting. Most apps solve this with two toggles. One to let the user decide
if they want the app to follow the system settings, or if the app should apply it’s own dark
and light modes. The second toggle would allow a user to choose between light and dark
mode. Without diving into the UI for these toggles, you could model a theming manager that
supports this as follows:
class ThemeManager {
var shouldOverrideSystemSetting: Bool {
get { UserDefaults.standard.bool(forKey:
,→
"ThemeManager.shouldOverrideSystemSetting") }
set { UserDefaults.standard.set(newValue, forKey:
,→
"ThemeManager.shouldOverrideSystemSetting") }
}
var shouldApplyDarkMode: Bool {
get { UserDefaults.standard.bool(forKey:
,→
"ThemeManager.shouldApplyDarkMode") }
set { UserDefaults.standard.set(newValue, forKey:
,→
"ThemeManager.shouldApplyDarkMode") }
}
}
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 codebase 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 CurrentValueSubject 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
}
lazy private(set) var themeSubject:
,→
CurrentValueSubject<PreferredUserInterfaceStyle, Never> = {
var preferredStyle = PreferredUserInterfaceStyle.system
if shouldOverrideSystemSetting {
preferredStyle = shouldApplyDarkMode ? .dark : .light
}
return CurrentValueSubject<PreferredUserInterfaceStyle,
,→
Never>(preferredStyle)
}()
var shouldOverrideSystemSetting: Bool {
get { UserDefaults.standard.bool(forKey:
,→
"ThemeManager.shouldOverrideSystemSetting") }
set {
UserDefaults.standard.set(newValue, forKey:
,→
"ThemeManager.shouldOverrideSystemSetting")
Donny Wals
77
Practical Combine
updateThemeSubject()
}
}
var shouldApplyDarkMode: Bool {
get { UserDefaults.standard.bool(forKey:
,→
"ThemeManager.shouldApplyDarkMode") }
set {
UserDefaults.standard.set(newValue, forKey:
"ThemeManager.shouldApplyDarkMode")
,→
updateThemeSubject()
}
}
private func updateThemeSubject() {
if shouldOverrideSystemSetting {
themeSubject.value = shouldApplyDarkMode ? .dark : .light
} else {
themeSubject.value = .system
}
}
}
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
override func viewDidLoad() {
// other setup code
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 subscribing to the themeManager’s themeSubject, the view controller can update its overrideUserInterfaceStyle 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 injecting 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:
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
Donny Wals
79
Practical Combine
let themeManager = ThemeManager()
window = UIWindow(windowScene: windowScene)
window?.rootViewController = MainViewController(themeManager:
themeManager)
,→
window?.makeKeyAndVisible()
}
The view controller would have the following initializer:
class MainViewController: UIViewController {
let themeManager: ThemeManager
init(themeManager: ThemeManager) {
self.themeManager = themeManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
}
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 @Published, 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
Using Combine to respond to user input
In the previous chapter, you learned how Combine can be used to drive your user interface.
The data streams I showed you were pretty static, and user interfaces are typically not this static.
Users should be able to tap, drag and swipe to their heart’s content in your app, manipulating
data and mutating your app’s state all the time. In this chapter, we’re going to kick it up a
notch and respond to user input. You will learn how you can accept user input in one place,
and update a UI component somewhere else on the screen. Once you understand how this
relationship works in UIKit, I’m going to take a brief detour to the world of SwiftUI, where this
principle is built-in through the Binding object.
You will also learn how you can place certain timing restrictions on processing user input to
prevent the user from generating more input than your code is willing to handle. This chapter
is divided into the following sections:
• Updating the UI based on user input
• Limiting the frequency of user input
• Combining multiple user inputs into a single publisher
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?
Updating the UI based on user input
In the previous chapter, you saw how you can use Combine’s assign(to:on:) method to
assign a publisher’s output directly to a UI element. While this is pretty cool, it’s also somewhat
limiting. The assign(to:on:) way of subscribing to a stream assigns data that’s emitted
by a publisher to a UI element, but wouldn’t it be far more interesting to somehow respond to
user interaction, update a property and then update the UI?
If you’ve worked with RxSwift before you started learning Combine, you might know that it
comes with several add-ons that provide a tight integration with UIKit components. Unfortunately, this kind of add-on is not natively available for Combine. In Combine, you need to
set up and configure custom extensions on UIKit elements to integrate your UI with Combine.
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:
class ViewController: UIViewController {
let slider = UISlider()
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// a bunch of setup code
updateLabel()
slider.addTarget(self, action: #selector(updateLabel), for:
.valueChanged)
,→
}
@objc func updateLabel() {
label.text = "Slider is at \(slider.value)"
}
}
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:
class ViewController: UIViewController {
let slider = UISlider()
let label = UILabel()
Donny Wals
83
Practical Combine
var sliderValue: Float = 50 {
didSet {
slider.value = sliderValue
label.text = "Slider is at \(sliderValue)"
}
}
override func viewDidLoad() {
super.viewDidLoad()
// a bunch of setup code
slider.addTarget(self, action: #selector(updateLabel), for:
.valueChanged)
,→
}
@objc func updateLabel() {
sliderValue = slider.value
}
}
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:
class ViewController: UIViewController {
let slider = UISlider()
let label = UILabel()
@Published var sliderValue: Float = 50
var cancellables = Set<AnyCancellable>()
Donny Wals
84
Practical Combine
override func viewDidLoad() {
super.viewDidLoad()
$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)
slider.addTarget(self, action: #selector(updateLabel), for:
.valueChanged)
,→
}
@objc func updateLabel() {
sliderValue = slider.value
}
}
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
Caution: Apple implemented a publisher(for:) on NSObject. This publisher
allows you to subscribe to KVO changes on classes that inherit from NSObject. UISlider inherits from NSObject through its superclass UIControl. This means that
you could use slider.publisher(for: \.value) to subscribe to KVO updates
for the slider’s value. Unfortunately, UIControl subclasses aren’t KVO-compliant.
This means that slider.publisher(for: \.value) doesn’t work for sliders and
other controls.
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:
struct ExampleView: View {
@State private var sliderValue: Float = 50
var body: some View {
VStack {
Text("Slider is at \(sliderValue)")
Slider(value: $sliderValue, in: (1...100))
}
}
}
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.
Limiting the frequency of user input
Another common situation where you need to respond to user input is when the user provides
input that you need to make a network request. For example, in a search field. A good search
field is responsive to the user’s input and makes requests to your search API without forcing
the user to press a dedicated “search” button. At the same time, if a user is typing quickly, you
don’t want to make a new request for every single character that they type. For example, you
might want to make sure that the user has typed more than an arbitrary number of characters
before you even initiate a request to your search endpoint in the first place.
Additionally, a user that is still typing is unlikely to be interested in the search result for every
character they have typed. So we can save precious bandwidth by waiting for a little while
before we process the user input to make sure that they aren’t still in the middle of typing
their query. For instance, we could wait for 300 milliseconds after the last typed character to
begin loading data.
I chose a threshold of 300 milliseconds because that’s a commonly used threshold that I’ve
seen used for similar features.
The principle of waiting a little while before processing user input is called debouncing, and
with Combine, we can implement debouncing rather easily. Before I show you how exactly,
I want to show you the code I’m working with to make sure it’s easy for you to follow along
with me:
Donny Wals
87
Practical Combine
class DebounceViewController: UIViewController {
let textField = UITextField()
let label = UILabel()
@Published var searchQuery: String?
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// a bunch of setup code
textField.addTarget(self, action: #selector(textChanged), for:
.editingChanged)
,→
$searchQuery
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
@objc func textChanged() {
searchQuery = textField.text
}
}
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
Figure 5: A marble diagram that describes the debounce operator
In this diagram, you can see that whenever a publisher emits several values in a short timeframe, 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 $searchQuery:
$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 absolutely 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:
Figure 6: A marble diagram that describes the filter operator
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
.assign(to: \.text, on: label)
.store(in: &cancellables)
If we go through the same sequence that I’ve just shown in the marble diagram, we’d see the
following output:
receive subscription: (Filter)
request unlimited
receive value: (Optional("Hello"))
receive value: (Optional("Hello"))
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
Figure 8: A marble diagram that describes a filter operator followed by a removeDuplicates
operator.
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:
receive subscription: (RemoveDuplicates)
request unlimited
receive value: (Optional("Hello"))
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
Figure 9: A marble diagram that shows throttle with latest: true
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
word count once every couple of seconds.
Combining multiple user inputs into a
single publisher
There are times where you need something other than a one on one mapping between a
publisher and a value. Consider a form where a user would input their details, and you display
a summary of the information they supplied below the form. The UI might look a bit as
follows:
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:
@Published var firstName = ""
@Published var lastName = ""
@Published var address = ""
@Published var zipCode = ""
@Published var country = ""
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 Combine.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.
Combining publishers with Publishers.Zip
The first of the three options I want to explore is Publishers.Zip. This publisher takes
two publishers and zips them together. In addition to Publishers.Zip, you can also
Donny Wals
99
Practical Combine
use Publishers.Zip3 and Publishers.Zip4 these alternative zip publishers take
three or four publishers to zip depending on the version you use. You might wonder why
Publishers.Zip doesn’t just work on an array instead of having a couple of versions that
take a different number of arguments. The reason for this complicated, but what it comes
down to is that a version of Zip that accepts an array of publishers would need to erase all
type information of the publishers that it zips. If you’re interested in learning more about
this, I would recommend that you do some research into the term variadic generics which is a
feature that will (hopefully) be added to Swift in the future and would allow for more complex
and advanced ways to express a list of generic objects.
To demonstrate all three merging publishers, I’m going to use NotificationCenter. I will
create two publishers that listen for different notifications. I will then use a merging publisher,
in this case Zip to combine the two publishers. Then, I will fire the two notifications one by
one to see what happens. I will do this twice. This here is the skeleton code for this process:
var cancellables = Set<AnyCancellable>()
let firstNotificationName = Notification.Name("first")
let secondNotificationName = Notification.Name("second")
let firstNotification = Notification(name: firstNotificationName)
let secondNotification = Notification(name: secondNotificationName)
let first = NotificationCenter.default.publisher(for:
,→
firstNotificationName)
let second = NotificationCenter.default.publisher(for:
,→
secondNotificationName)
// create and subscribe to Zip, Merge and CombineLatest
print("send first")
NotificationCenter.default.post(firstNotification)
print("send second")
NotificationCenter.default.post(secondNotification)
Donny Wals
100
Practical Combine
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:
let zipped = Publishers.Zip(first, second).sink(receiveValue: { val in
print(val, "zipped")
}).store(in: &cancellables)
Simple enough, right? Let’s see what the output in the console looks like for Publishers.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)
Donny Wals
101
Practical Combine
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:
Figure 13: A marble diagram that shows the zip operator.
In addition to creating an instance of Publishers.Zip directly, it’s also possible to apply
the zip operator to a publisher instead:
Donny Wals
102
Practical Combine
let zipped = first.zip(with: second).sink(receiveValue: { val in
print(val, "zipped")
}).store(in: &cancellables)
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:
let left = CurrentValueSubject<Int, Never>(0)
let right = CurrentValueSubject<Int, Never>(0)
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
This code produces the following output:
(0, 0)
(1, 1)
(2, 2)
As you can see, left emits three values before right emits its first value. Since the publishers are zipped in order, you can see that Publishers.Zip emits the first and second
emitted value from both publishers together.
Donny Wals
103
Practical Combine
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.
Combining publishers with Publishers.Merge
The second combining publisher I want to show you is Publisher.Merge. We’re going to
use the same code as I used for Publishers.Zip, except that instead of using Publishers.Zip, I will use Publishers.Merge. But before I do, let’s talk about the different
versions of Publishers.Merge. Similar to Publishers.Zip, there are flavors that take
two, three or four publishers to merge them into one publisher. But Apple didn’t stop there.
They went all the way up to Publishers.Merge8 that allows you to merge eight publishers. And as if that’s not enough, Apple also added a Publishers.MergeMany variant that
takes an arbitrary number of publishers. All flavors of Publishers.Merge require that
the publishers you want to merge have the same Output and Failure types. The only
major difference between Publishers.MergeMany and the other versions of Publishers.Merge that I was able to discover is that Publishers.MergeMany allows you to
inspect the publishers it’s merging.
You can use Publishers.Merge as follows:
let merged = Publishers.Merge(first, second).sink(receiveValue: { val
,→
in
print(val, "merged")
}).store(in: &cancellables)
Alternatively, you can use the merge operator, similar to how you were able to use the zip
operator earlier:
let merged = first.merge(with: second).sink(receiveValue: { val in
print(val, "merged")
}).store(in: &cancellables)
Donny Wals
104
Practical Combine
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:
Figure 14: A marble diagram that explains the merge operator.
This is really close to the desired effect. If we’d apply this to the form that I showed you at the
Donny Wals
105
Practical Combine
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.
Combining publishers with Publishers.CombineLatest
The last publisher I want to show you is Publishers.CombineLatest. This publisher
has several versions, just like Publishers.Merge and Publishers.Zip. For Publishers.CombineLatest, Apple has created three versions in total. In addition to Publishers.CombineLatest we have Publishers.CombineLatest3 and Publishers.CombineLatest4. These exist for the same reasons that Publishers.Zip has
multiple versions and they work in the exact same way. So without further explanation, let’s
look at the code needed to use Publishers.CombineLatest in our little test skeleton:
let combined = Publishers.CombineLatest(first,
,→
second).sink(receiveValue: { val in
print(val, "combined")
}).store(in: &cancellables)
Or written with the combineLatest operator:
let combined = first.combineLatest(second).sink(receiveValue: { val in
print(val, "combined")
}).store(in: &cancellables)
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
Donny Wals
106
Practical Combine
(name = first, object = nil, userInfo = nil, name = second, object =
,→
nil, userInfo = nil) combined
send fourth
(name = first, object = nil, userInfo = nil, name = second, object =
,→
nil, userInfo = nil) combined
Once both publishers have emitted an initial value, the Publishers.CombineLatest
publisher will publish the last value emitted for each publisher. The following marble diagram
describes Publishers.CombineLatest:
Figure 15: A marble diagram that explains combineLatest.
This combining publisher is pretty much ideal for our purposes. The only caveat is that both
Donny Wals
107
Practical Combine
@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
Donny Wals
108
Practical Combine
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.
Donny Wals
109
Practical Combine
Using Combine for networking
Networking is a key component in many modern applications and it’s no surprise that Apple
decided to add a special extension to URLSession to integrate networking with Combine
nicely and fluently. Networking is a fantastic use case for Combine because it’s an asynchronous task with a certain output, that you’ll probably want to manipulate and transform
before using it.
In this chapter, I will show you how can take an existing networking implementation and
refactor it to make it play nicely with Combine. This chapter is divided into the following
topics:
•
•
•
•
Creating a simple networking layer in Combine
Decoding JSON data with Combine
User-friendly networking with Combine
Building a complex chain of network calls
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.
Creating a simple networking layer in
Combine
If you’ve worked on applications for iOS, tvOS, or even macOS you have probably written
or worked with networking code at some point during the development cycle. Your code
probably looked a bit like this:
func fetchURL<T: Decodable>(_ url: URL, completion: @escaping
,→
(Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: myURL) { data, response, error in
guard let data = data else {
if let error = error {
Donny Wals
110
Practical Combine
completion(.failure(error))
}
assertionFailure("Callback got called with no data and no
,→
error. This should never happen.")
return
}
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.
Donny Wals
111
Practical Combine
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: Decodable>(_: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:
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ result in
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: result.data)
})
.eraseToAnyPublisher()
}
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,
Donny Wals
112
Practical Combine
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:
let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL)
And what’s interesting is that you can subscribe to this publisher multiple times to get its
result:
var cancellables = Set<AnyCancellable>()
let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL)
publisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { (model: MyModel) in
print(model)
Donny Wals
113
Practical Combine
}).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:
let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL)
.share()
.eraseToAnyPublisher()
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.
Donny Wals
114
Practical Combine
Handling JSON responses from a network
call
More often than not, the data returned by a data task will be JSON data. To make decoding this
data easy for you, Combine has a decode operator that works on publishers that have Data
as their output. Let’s see how this operator can be used in the fetchURL(_:) function I
showed you in the previous section:
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
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 PropertyListDecoder. 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:
Donny Wals
115
Practical Combine
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.mapError({ $0 as Error })
.flatMap({ result -> AnyPublisher<T, Error> in
guard let urlResponse = result.response as? HTTPURLResponse,
,→
(200...299).contains(urlResponse.statusCode) else {
fatalError("We'll handle this later")
}
return Just(result.data).decode(type: T.self, decoder:
,→
JSONDecoder()).eraseToAnyPublisher()
}).eraseToAnyPublisher()
}
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 received 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.
Donny Wals
116
Practical Combine
Let’s expand the code I just showed you by adding the code that’s supposed to be where I
initially wrote a fatalError:
struct APIError: Decodable, Error {
// fields that model your error
}
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.mapError({ $0 as Error })
.flatMap({ result -> AnyPublisher<T, Error> in
guard let urlResponse = result.response as? HTTPURLResponse,
,→
(200...299).contains(urlResponse.statusCode) else {
return Just(result.data)
.decode(type: APIError.self, decoder: JSONDecoder())
.tryMap({ errorModel in
throw errorModel
})
.eraseToAnyPublisher()
}
return Just(result.data).decode(type: T.self, decoder:
,→
JSONDecoder()).eraseToAnyPublisher()
}).eraseToAnyPublisher()
}
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:
Donny Wals
117
Practical Combine
var cancellables = Set<AnyCancellable>()
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 implementation 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:
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ result in
let decoder = JSONDecoder()
guard let urlResponse = result.response as? HTTPURLResponse,
,→
(200...299).contains(urlResponse.statusCode) else {
let apiError = try decoder.decode(APIError.self, from:
,→
result.data)
throw apiError
}
return try decoder.decode(T.self, from: result.data)
})
.eraseToAnyPublisher()
}
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
Donny Wals
118
Practical Combine
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 switchToLatest 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.
Tip: I first introduced flatMap in Chapter 3 - Transforming publishers. Go back to that
chapter to read up on maxPublishers if you’re not entirely sure how that argument
works.
Now that you have an idea of what switchToLatest does, let’s look at this operator as a
marble diagram:
Donny Wals
119
Practical Combine
Figure 16: A marble diagram that explains map and switchToLatest
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:
struct APIError: Decodable, Error {
let statusCode: Int
}
func refreshToken() -> AnyPublisher<Bool, Never> {
Donny Wals
120
Practical Combine
// normally you'd have your refresh logic here
return Just(false).eraseToAnyPublisher()
}
func fetchURL<T: Decodable>(_ url: URL) -> AnyPublisher<T, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ result in
let decoder = JSONDecoder()
guard let urlResponse = result.response as? HTTPURLResponse,
,→
(200...299).contains(urlResponse.statusCode) else {
let apiError = try decoder.decode(APIError.self, from:
,→
result.data)
throw apiError
}
return try decoder.decode(T.self, from: result.data)
})
.tryCatch({ error -> AnyPublisher<T, Error> in
guard let apiError = error as? APIError, apiError.statusCode ==
,→
401 else {
throw error
}
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 APIError which has a statusCode property. In my implementation, that statusCode would
Donny Wals
121
Practical Combine
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 purposes, 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 complicated 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.
Donny Wals
122
Practical Combine
User-friendly networking with Combine
Aside from the straightforward network calls you learned about in the previous section, there
are certain networking features on iOS that are somewhat important and often overlooked.
There are two features that I want to focus on in this section because they are new in iOS 13,
are very useful for your users, and they integrate very nicely with Combine.
In iOS 13, Apple introduced two new features that allow developers to help their users save
precious data. The features I’m talking about are Low Data Mode and the allowsExpensiveNetworkAccess property on URLSessionConfiguration.
Low Data mode is a feature that users can enable in the settings app under their networking
settings. When Low Data mode is active, the user provides a clear sign to apps that they’re
using. The network they are on is slow, congested, they may have a limited data plan, or they
may have Low Data mode enabled for a difference. Ultimately the reason doesn’t really matter.
The user has a clear preference and as developers, we should respect this preference as much
as possible.
When Low Data mode is active, your app has the same capabilities as it always had. The feature
is opt-in from an application’s point of view. It’s good practice to do this and to consider which
parts of your app are non-essential, use a lot of data, or can easily be replaced with a version
that uses fewer of the user’s precious megabytes. A good example of a feature that can easily
be adapted for Low Data mode is a fancy screen that uses a nice gif or video as a background.
When Low Data mode is active, you might choose to load a static image as a background
instead of having an animated version. Alternatively, you might load lower resolution images
when Low Data mode is active.
Before I move on to explain allowsExpensiveNetworkAccess, let’s see how you can
opt-in to Low Data mode, and make it play nicely with Combine.
The simplest way to opt-in to Low Data mode is to set the allowsConstrainedNetworkAccess property on your URLSessionConfiguration to false:
let session = URLSession.shared
session.configuration.allowsConstrainedNetworkAccess = false
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-
Donny Wals
123
Practical Combine
strained. For that reason, I wouldn’t recommend enabling low data mode on the configuration for URLSession.shared. Instead, you might want to consider two separate
URLSession instances for this purpose:
let session = URLSession.shared
let constrainedConfiguration = URLSessionConfiguration.default
constrainedConfiguration.allowsConstrainedNetworkAccess = false
let constrainedSession = URLSession(configuration:
,→
constrainedConfiguration)
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:
var constrainedRequest = URLRequest(url: fullImageURL)
constrainedRequest.allowsConstrainedNetworkAccess = false
let normalRequest = URLRequest(url: smallImageURL)
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
}
Donny Wals
124
Practical Combine
return session.dataTaskPublisher(for: normalRequest)
})
.sink(receiveCompletion: { completion in
// handle completion
}, receiveValue: { result in
// handle received data
})
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 allowsExpensiveNetworkAccess 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.
Donny Wals
125
Practical Combine
Building a complex chain of network
calls
In the previous chapter I showed you how you can use a Publishers.CombineLatest
publisher to combine values that are emitted by two publishers into a publisher that emits
tuples of the two latest values that were emitted by both publishers. Merging or combining
the output of publishers isn’t only useful in the context of UI. It can also be really useful if you
want to use Combine to make sure several network calls have succeeded, or if you want to
fire off multiple network calls and handle their responses in a single sink. In this section, I
will show you how you can use Combine’s Publishers.Zip and Publishers.Merge
publishers to create an interesting and complicated homepage building process with very
little code.
The following image describes the flow of the process that I will show you in this section:
Figure 17: The flow for building the homepage
Note that the homepage in this image is made up of three tasks. One task fetches the featured
Donny Wals
126
Practical Combine
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:
enum SectionType: String, Decodable {
case featured, favorites, curated
}
struct Event: Decodable, Hashable {
// event properties
}
struct HomePageSection {
let events: [Event]
let sectionType: SectionType
static func featured(events: [Event]) -> HomePageSection {
return HomePageSection(events: events, sectionType: .featured)
}
static func favorites(events: [Event]) -> HomePageSection {
return HomePageSection(events: events, sectionType: .favorites)
}
static func curated(events: [Event]) -> HomePageSection {
return HomePageSection(events: events, sectionType: .curated)
}
Donny Wals
127
Practical Combine
}
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:
var featuredPublisher = URLSession.shared.dataTaskPublisher(for:
,→
featuredContentURL)
.map({ $0.data })
.decode(type: [Event].self, decoder: JSONDecoder())
.replaceError(with: [Event]())
.map({ HomePageSection.featured(events: $0) })
.eraseToAnyPublisher()
var curatedPublisher = URLSession.shared.dataTaskPublisher(for:
,→
curatedContentURL)
.map({ $0.data })
.decode(type: [Event].self, decoder: JSONDecoder())
.replaceError(with: [Event]())
.map({ HomePageSection.curated(events: $0) })
.eraseToAnyPublisher()
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:
Donny Wals
128
Practical Combine
class LocalFavorites {
static func fetchAll() -> AnyPublisher<[Event], Never> {
// retrieve events from a local source
}
}
var localFavoritesPublisher = LocalFavorites.fetchAll()
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:
var favoritesPublisher = Publishers.Zip(localFavoritesPublisher,
remoteFavoritesPublisher)
,→
.map({ favorites -> HomePageSection in
let uniqueFavorites = Set(favorites.0 + favorites.1)
return HomePageSection.favorites(events: Array(uniqueFavorites))
})
.eraseToAnyPublisher()
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 homePagePublisher which will merge the favoritesPublisher, curatedPublisher and
Donny Wals
129
Practical Combine
featuredPublisher together which will allow us to build the homepage as sections
become available rather than waiting for all sections to be fetched and rendering the entire
page at once:
var homePagePublisher = Publishers.Merge3(featuredPublisher,
,→
curatedPublisher, favoritesPublisher)
This code is pretty straightforward. We call Publishers.Merge3 to create a new publisher
that will interleave the events from the three publishers it merges which means that subscribers of the homePagePublisher will receive a single stream of values that represent
different sections of the homepage:
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.
Donny Wals
130
Practical Combine
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.
Donny Wals
131
Practical Combine
Wrapping existing asynchronous
processes with Futures in Combine
In this book, I have mostly focussed on showing you built-in Combine publishers, operators,
and processes. While this is a fantastic foundation to work with, the reality is that most code
bases contain tons of custom asynchronous operations. Several of Apple’s frameworks come
with asynchronous jobs that are not (yet) wrapped in Combine publishers.
Luckily, Combine comes with the ability to create custom publishers in the form of Subject objects like CurrentValueSubject and PassthroughSubject. These subjects
are extremely useful to turn mutable properties into publishers that parts of your app can
subscribe to. You can even use them to create a publisher that performs a task and outputs a single result. Oftentimes, you’ll find that creating a CurrentValueSubject or a
PassthroughSubject will give you the kind of abstraction you’re looking for.
In this chapter, I would like to present an alternative abstraction to you called Future. A
Future in Combine is a special kind of publisher that performs work and fulfills a promise
to notify subscribers that the work is done. This sounds very similar to how a Publisher
works, but a Future in Combine comes with special semantics that makes it look similar to
other publishers, while it acts in a completely different way.
In this chapter, you will learn everything you need to know about futures. You will learn how
they work, how they differ from other publishers, and when they might be useful in your
apps.
Understanding how Futures work
If you’ve worked with abstractions over asynchronous takes before, you may have heard of
the term Future, or Promise. The concept of futures is not unique to Combine and there
are several libraries in different programming languages on different platforms that provide
an implementation of the Future object. A Future in Combine is a publisher that will
eventually emit a single value and completes immediately. For most publishers other than
Just this rule is somewhat of an unwritten rule because it’s not strictly enforced. If you look
Donny Wals
132
Practical Combine
at a publisher like URLSession.DataTaskPublisher, nothing about it tells you that it
will only emit a single value. You know it won’t emit multiple values, but theoretically, it can.
Or at least, it looks like it can. There is no way for us to be sure. This is especially true if you
transform the data task publisher to an AnyPublisher.
A Future in Combine can’t ever emit more than a single value because it’s enforced in its
implementation. Because a Future is a publisher, you can transform it into an AnyPublisher which would hide this detail from users of your Future, but at least you know at the
point where you create the Future what you’re working with. It’s a small difference that you
might consider to be insignificant, but as you’ll find out throughout this chapter, it matters.
Before I explain more about Future and how it compares to other publishers, I want to show
you a quick example of how a Future is created and used.
var cancellables = Set<AnyCancellable>()
func createFuture() -> Future<Int, Never> {
return Future { promise in
promise(.success(Int.random(in: 0..<Int.max)))
}
}
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?
Donny Wals
133
Practical Combine
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:
var cancellables = Set<AnyCancellable>()
func fetchURL(_ url: URL) -> Future<(data: Data, response:
URLResponse), URLError> {
,→
return Future { promise in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error as? URLError {
promise(.failure(error))
}
if let data = data, let response = response {
promise(.success((data: data, response: response)))
}
}.resume()
}
}
let publisher = fetchURL(myURL)
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 respective 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:
Donny Wals
134
Practical Combine
var cancellables = Set<AnyCancellable>()
let publisher: AnyPublisher<MyModel, Error> = fetchURL(myURL)
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
let myURL = URL(string: "https://practicalcombine.com")!
func fetchURL(_ url: URL) -> Future<(data: Data, response:
,→
URLResponse), URLError> {
return Future { promise in
Donny Wals
135
Practical Combine
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error as? URLError {
promise(.failure(error))
}
if let data = data, let response = response {
promise(.success((data: data, response: response)))
}
print("RECEIVED RESPONSE")
}.resume()
}
}
let publisher = fetchURL(myURL)
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:
Donny Wals
136
Practical Combine
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:
Caution: A Future executes immediately when it is created instead of waiting for a
subscriber like a normal publisher does. This means that even though Future looks
and feels like a Publisher it doesn’t play by the same rules.
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.
Donny Wals
137
Practical Combine
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:
func createFuture() -> Future<Int, Never> {
return Future { promise in
promise(.success(Int.random(in: (1...100))))
}
}
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:
func createDeferredFuture() -> Deferred<Future<Int, Never>> {
return Deferred {
return Future { promise in
promise(.success(Int.random(in: (1..<Int.max))))
}
}
}
Notice that I renamed the function from createFuture() to createDeferredFuture(). I did this so you can easily tell them apart.
The return type changed from Future<Int, Never> to Deferred<Future<Int,
Never>>. This informs anybody that calls createDeferredFuture() that they can
expect the following statements to be true about the publisher they receive:
Donny Wals
138
Practical Combine
• The publisher will not perform work until it has subscribers
• If you subscribe to the same instance of this publisher more than once, the work will be
repeated
Note that both of these statements are false for a regular Future. The following code demonstrates this:
let plainFuture = createFuture()
plainFuture.sink(receiveValue: { value in
print("plain1", value)
})
plainFuture.sink(receiveValue: { value in
print("plain2", value)
})
let deferred = createDeferredFuture()
deferred.sink(receiveValue: { value in
print("deferred1", value)
})
deferred.sink(receiveValue: { value in
print("deferred2", value)
})
Here’s an example of the output produced by the code above:
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
Donny Wals
139
Practical Combine
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:
let manuallyDeferredPublisher = Deferred {
return createFuture()
}
The Future created by createFuture() is not executed until manuallyDeferredPublisher receives a subscriber.
If you do decide that you want to return a deferred Future from a method, I would recommend type erasing your publisher to AnyPublisher. This will make it clear to users of
Donny Wals
140
Practical Combine
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 understand, 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.
Using Combine to ask for push
permissions
I think futures are a great way to wrap asynchronous work that you would normally kick off
immediately and don’t need future updates for. In other words, in my opinion, a Future is
a good abstraction for asynchronous work that I want to write as if I’m not using Combine
while in reality, I am. By using futures for this kind of work I get all of the benefits of Combine
without the downsides of imperative asynchronous code. Consider a scenario where you
want to check for a user’s current push notification permissions, and take action based on the
result of that call. You might write the following code to get notification permissions and take
action based on the current permission status if you’re not using Combine:
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
Donny Wals
141
Practical Combine
if result == true && error == nil {
// We have notification permissions
} else {
DispatchQueue.main.async {
// Something went wrong / we don't have permission.
// update UI to point user to settings
}
}
}
default:
// assume permissions are fine and proceed
break
}
}
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:
1. Get the user’s current notification settings.
2. Ask the user for permission to send notifications if we haven’t already.
Both of these tasks can be written as a Future:
Donny Wals
142
Practical Combine
extension UNUserNotificationCenter {
func getNotificationSettings() -> Future<UNNotificationSettings,
Never> {
,→
return Future { promise in
self.getNotificationSettings { settings in
promise(.success(settings))
}
}
}
func requestAuthorization(options: UNAuthorizationOptions) ->
Future<Bool, Error> {
,→
return Future { promise in
self.requestAuthorization(options: options) { result, error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(result))
}
}
}
}
}
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
Donny Wals
143
Practical Combine
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:
var cancellables = Set<AnyCancellable>()
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.
Donny Wals
144
Practical Combine
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.
Using Futures to fetch data from Core
Data
If you’ve worked with Core Data before, you probably know how finicky threading can be
in a Core Data environment. In my experience, you often end up writing fetch requests as
follows:
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
func fetchUsers(_ completion: @escaping ([User]) -> Void) {
persistentContainer.fetchUsers(using:
,→
persistentContainer.viewContext, completion: completion)
Donny Wals
145
Practical Combine
}
}
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:
func fetchUsers(using moc: NSManagedObjectContext) -> Future<[User],
Never> {
,→
return Future { promise in
moc.perform {
let request: NSFetchRequest<User> = User.fetchRequest()
let users = try? moc.fetch(request)
promise(.success(users ?? []))
}
}
}
struct UsersFetcher {
let persistentContainer: NSPersistentContainer
func fetchUsers() -> Future<[User], Never> {
return persistentContainer.fetchUsers(using:
,→
persistentContainer.viewContext)
}
}
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:
Donny Wals
146
Practical Combine
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 immediately 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 UNUserNotificationCenter 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.
Donny Wals
147
Practical Combine
Understanding Combine’s Schedulers
A topic I haven’t touched upon in-depth so far is the topic of Combine’s schedulers. In this
chapter, I aim to change this. I will explain what Combine’s schedulers are, what they do
and more importantly how and when you will run into them when you work with Combine.
Because this book is all about providing you with useful knowledge that you can apply in the
real world. I won’t get down to the finest of details about this topic. Schedulers in Combine
are heavily tied to dispatch queues, threads, and the runloop and I’m pretty sure I won’t be
able to do all the elaborate details of those topics justice in a single chapter.
By the end of this chapter, you will have a good understanding of what Combine’s Scheduler
protocol is and what Combine’s receive(on:) and subscribe(on:) operators mean
and do.
Exploring the Scheduler protocol
Because Combine deals with tons of asynchronous work, Combine must have a good way to
schedule and execute work without blocking any threads. To help with this, Combine defines
a protocol called Scheduler that defines all kinds of requirements that an object capable
of performing work on behalf of Combine’s publishers. By default, several objects that you
might be familiar with already conform to the Scheduler protocol:
• RunLoop
• DispatchQueue
• OperationQueue
Some of the possibly lesser-known objects that conform to Scheduler include
OS_dispatch_queue_main and OS_dispatch_queue_global. These last two
objects are not created by you directly. Instead, you typically gain access to them through
DispatchQueue.main and DispatchQueue.global() respectively.
Combine also provides a scheduler of its own called ImmediateScheduler which is a
scheduler that always performs work that it’s asked to perform immediately. Any attempts
to schedule a delayed task on this scheduler will result in immediate execution rather than
delayed execution.
Donny Wals
148
Practical Combine
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 debouncing 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)
Because Scheduler is a protocol, it’s possible to swap out DispatchQueue.global()
for an instance of OperationQueue or a RunLoop. Ultimately they should provide the
same functionality. Choosing one is a matter of context and choice in some cases. For example,
RunLoop.main and DispatchQueue.main are both schedulers that operate on the main
thread.
Using an instance of OperationQueue as a scheduler is a bit trickier. In Combine, it’s
expected that all schedulers operate as a serial queue. This means that every scheduler
handles tasks one by one rather than in parallel. This is an important detail because Combine
has no way of enforcing this and you might run into some interesting problems if you end up
Donny Wals
149
Practical Combine
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.
Understanding receive(on:) and
subscribe(on:)
In Combine, there are two main operators that you can apply to your publishers that have a
huge impact on the behavior of your code. These operators are receive and subscribe.
Both take a Scheduler as their argument. While these operators look somewhat similar,
their uses are very different from each other.
In the previous chapter, I’ve shown you the receive(on:) publisher using the following
code:
var cancellables = Set<AnyCancellable>()
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()
}
Donny Wals
150
Practical Combine
})
.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:
var cancellables = Set<AnyCancellable>()
let intSubject = PassthroughSubject<Int, Never>()
intSubject.sink(receiveValue: { value in
print(value)
print(Thread.current)
}).store(in: &cancellables)
intSubject.send(1)
DispatchQueue.global().async {
intSubject.send(2)
}
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = DispatchQueue(label: "com.donnywals.queue")
Donny Wals
151
Practical Combine
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.
Donny Wals
152
Practical Combine
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:
let intSubject = PassthroughSubject<Int, Never>()
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)
If you run this code in a Playground (don’t forget to set PlaygroundPage.current.needsIndefiniteE
= true), you will find that sometimes all three values are printed, but usually only the second
and third value are printed. The reason for this is that the subscription to intSubject is now
performed on the global dispatch queue which means it’s performed asynchronously on a
different thread than the one we’re sending values from. This means that the first and second
values may have already been sent before the subscription is set up. Try replacing DispatchQueue.global() in this example with ImmediateScheduler.shared. You’ll
see that the subscription is now executed immediately. For a subject like PassthroughSubject, it’s unlikely that you’ll ever want to apply subscribe(on:). But there are cases
where it’s extremely useful:
func generateInt() -> Future<Int, Never> {
return Future { promise in
promise(.success(Int.random(in: 1...10)))
Donny Wals
153
Practical Combine
}
}
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 subscribe(on) around to see what happens:
Donny Wals
154
Practical Combine
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 subscribe(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 subscriptions 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
Donny Wals
155
Practical Combine
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 subscribe(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.
Donny Wals
156
Practical Combine
After explaining receive(on:), I showed you subscribe(on:). This operator manipulates the scheduler that is used to create subscriptions on. You saw that this means that
values can get lost in the ether when you use DispatchQueue.global() to subscribe
to a PassthroughSubject that is expected to output values immediately. After that less
than useful example, I showed you how you can use subscribe(on:) to move work that’s
done in a map to a background thread to prevent blocking the main thread. Note that this
isn’t guaranteed to always work because publishers can decide for themselves whether they
want to emit values on the same thread that they received that subscriber on.
Donny Wals
157
Practical Combine
Building your own Publishers,
Subscribers, and Subscriptions
You’ve come a long way since you started learning Combine in Chapter 1 - Introducing Functional Reactive Programming. This chapter is the last chapter where I’ll show you how to
use Combine, and it’s going to be a good one. You know how you can work with Combine in
your apps now. You know exactly how Combine’s operators work, and how you can chain
them together to build extremely powerful features with very little noise and boilerplate in
your code.
While Combine comes with a ton of built-in publishers and features, and Apple even actively
discourages developers from building their own custom publishers, there are times where a
custom publisher will scratch that itch that prevents you from writing the code you want to
write. And even if you end up staying away from custom publishers in your app, I think it’s
good that you have some insight into how publishers and subscribers work in Combine, and
how they are tied together.
In this chapter, I will show you how you can implement your own publishers. I will start by
examining a built-in publisher and the sink subscriber. By examining the behavior of this
pair, I will implement custom versions of this. Note that I will show you my interpretation and
not the official implementation. My interpretation will work as advertised, but I know it’s not
identical to Apple’s code.
By implementing a custom publisher, we’ll run into some new terminology in Combine that I
have been avoiding up until now. After reading this chapter, I truly believe that you will unlock
Combine’s full potential because you will have a thorough understanding of Combine’s core
principles. and more importantly, you’ll understand how you can use Combine effectively in
your apps.
Donny Wals
158
Practical Combine
Understanding what happens when you
subscribe to a publisher
Throughout this book, you have seen countless code samples where I used sink to subscribe
to a publisher. Every time the publisher emitted a new value, the sink’s receiveValue or
receiveCompletion closures would be called and you know exactly what each of these
closures does. What I haven’t looked into in-depth, is what happens when you call sink on a
publisher.
If you examine the documentation for sink, you’ll find that it’s a method defined on the
Publisher protocol. This is why you can call sink on any publisher in the framework. If
you do some more digging, you’ll find that Combine contains a Subscribers enum that
contains a Sink object. Based on this, I think it’s safe to say that sink creates one of these
Sink subscribers and subscribes it to the publisher. With this information, we can define a
custom version of sink that looks as follows:
extension Publisher {
func customSink(receiveCompletion: @escaping
(Subscribers.Completion<Self.Failure>) -> Void,
,→
receiveValue: @escaping (Self.Output) -> Void) ->
AnyCancellable {
,→
let sink = Subscribers.Sink(receiveCompletion: receiveCompletion,
,→
receiveValue: receiveValue)
self.subscribe(sink)
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
Donny Wals
159
Practical Combine
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:
var cancellables = Set<AnyCancellable>()
let publisher = [1, 2, 3].publisher
let subject = PassthroughSubject<Int, Never>()
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 chapter. 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
Donny Wals
160
Practical Combine
uses its receive(subscriber:) method to receive new subscribers and to call several
methods on the subscriber to interact with it:
• receive(subscription:) is called on the subscriber to pass it a subscription. The
subscriber can use this subscription to demand new items and to cancel the subscription.
I will explain this in-depth later in this chapter.
• receive(_:) is called on a subscriber to pass it a new value.
• receive(completion:) is called on a subscriber to pass it a completion event.
These three methods happen to be methods that are required by the Subscriber protocol.
Implementing a custom Subscriber
Because I started this section off with a custom implementation of the sink operator I think
it’s fitting to implement a custom version of Subscriber.Sink to see the three receive
methods I just listed in action.
extension Subscribers {
class CustomSink<Input, Failure: Error>: Subscriber {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
var subscription: Subscription?
init(receiveCompletion: @escaping
,→
(Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}
func receive(subscription: Subscription) {
self.subscription = subscription
Donny Wals
161
Practical Combine
subscription.request(.unlimited)
}
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}
}
}
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 receive(_:). 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
Donny Wals
162
Practical Combine
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)
To wrap a Subscriber in an AnyCancellable, it needs to conform to Cancellable.
We can make Subscribers.CustomSink conform to Cancellable quite easily:
extension Subscribers.CustomSink: Cancellable {
func cancel() {
subscription?.cancel()
subscription = nil
}
}
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 {
,→
let sink = Subscribers.CustomSink(receiveCompletion:
,→
receiveCompletion, receiveValue: receiveValue)
self.subscribe(sink)
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
Donny Wals
163
Practical Combine
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:
func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1))
}
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.
Donny Wals
164
Practical Combine
In my custom sink I implemented receive(_:) as follows:
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
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:
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .max(1)
}
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 delivered 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 backpressure 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.
Donny Wals
165
Practical Combine
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 implemented so you can make your own choices when the time comes to decide whether you
should create a custom publisher.
Understanding how you can create a custom publisher and
subscription
Creating a custom publisher is an interesting task because Apple doesn’t provide many details
on how to do it. All we know is that an object that conforms to the Publisher protocol
has to implement a receive(_:) method, and that we’re expected to connect a subscription and a subscriber in this method. What’s interesting is that we can derive a little bit
of behavior or information from the way things work in Combine. Remember that I used
URLSession.DataTaskPublisher to explain and demonstrate that a publisher doesn’t
begin working until it receives a subscriber? And that multiple subscriptions to the same
URLSession.DataTaskPublisher result in multiple network calls being made? This
tells us something interesting about publishers.
A single publisher does not necessarily generate a single stream of values. In other words,
a publisher might not be responsible for generating its own values. This is an interesting
thought that we can explore a little bit deeper. Consider the following code:
var cancellables = Set<AnyCancellable>()
let ints = [1, 2, 3].publisher
ints
.sink(receiveValue: { print($0 )})
.store(in: &cancellables)
ints
.sink(receiveValue: { print($0 )})
.store(in: &cancellables)
Donny Wals
166
Practical Combine
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
let numberOfValues: Int
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:
Donny Wals
167
Practical Combine
extension Subscriptions {
class IntSubscription<S: Subscriber>: Subscription where S.Input ==
Int, S.Failure == Never {
,→
let numberOfValues: Int
var currentValue = 0
var subscriber: S?
var openDemand = Subscribers.Demand.none
init(numberOfValues: Int, subscriber: S) {
self.numberOfValues = numberOfValues
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
openDemand += demand
while openDemand > 0 && currentValue < numberOfValues {
if let newDemand = subscriber?.receive(currentValue) {
openDemand += newDemand
}
currentValue += 1
openDemand -= 1
}
if currentValue == numberOfValues {
subscriber?.receive(completion: .finished)
cancel()
}
}
func cancel() {
subscriber = nil
Donny Wals
168
Practical Combine
// we don't have anything to clean up
}
}
}
It certainly is a lot more code, and the interesting bit is the request(_:) method’s implementation.
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 section:
let customPublisher = Publishers.IntPublisher(numberOfValues: 10)
customPublisher
.customSink(receiveCompletion: { completion in
Donny Wals
169
Practical Combine
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.
Extending UIControl with a custom
publisher
The idea of extending UIControl with a Combine publisher is no way unique. There is a
GitHub repository with all kinds of extensions for UIKit components that define custom
publishers that you can use in your projects. The code that I’m about to show you in this
section is heavily inspired by this project, except it’s somewhat simplified.
The final code I want to end up with is the following:
slider
.publisher(for: .valueChanged)
.sink(receiveValue: { control in
guard let slider = control as? UISlider
else { return }
Donny Wals
170
Practical Combine
print(slider.value)
}).store(in: &cancellables)
Looks good, doesn’t it?
To achieve this, we’ll need to extend UIControl with a method that allows us to call publisher(for:) with a UIControl.Event that represents the event we want to create a
publisher for. In the case of a text field, you are most likely interested in .valueChanged
but we should be able to pass any other UIControl.Event to publisher(for:) to
achieve maximum flexibility. The publisher(for:) method will create a custom publisher object. I’ll show you the publisher and its corresponding subscription first before I
define publisher(for:).
Before you read on though, think about what the code you’re about to read should look like for
a moment. You’ve already seen all of the core components of what we’re about to do, I’m just
going to change a couple of details. If you’re feeling particularly confident and adventurous,
I highly recommend to stop reading now and try to implement this entire publisher and
subscription pair yourself. See how far you get with it. I’m sure you’re going to do much better
than you might think!
The code for the publisher is pretty much identical to the publisher I’ve shown you before:
extension UIControl {
struct EventPublisher: Publisher {
typealias Output = UIControl
typealias Failure = Never
let control: UIControl
let controlEvent: UIControl.Event
func receive<S>(subscriber: S) where S : Subscriber, Failure ==
S.Failure, Output == S.Input {
,→
let subscription = EventSubscription(control: control, event:
,→
controlEvent, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
Donny Wals
171
Practical Combine
}
}
The publisher is initialized with an instance of UIControl and the UIControl.Event
that this publisher is expected to send values for. Note that I’ve used the UIControl itself
as the publisher’s Output. This makes it easy for subscribers to know what the origin of the
value was, and they can use the provided UIControl instance to read new values from.
Let’s look at the far more exciting EventSubscription object next:
extension UIControl {
class EventSubscription<S: Subscriber>: Subscription
where S.Input == UIControl, S.Failure == Never {
let control: UIControl
let event: UIControl.Event
var subscriber: S?
var currentDemand = Subscribers.Demand.none
init(control: UIControl, event: UIControl.Event, subscriber: S) {
self.control = control
self.event = event
self.subscriber = subscriber
control.addTarget(self,
action: #selector(eventOccured),
for: event)
}
func request(_ demand: Subscribers.Demand) {
currentDemand += demand
}
func cancel() {
subscriber = nil
Donny Wals
172
Practical Combine
control.removeTarget(self,
action: #selector(eventOccured),
for: event)
}
@objc func eventOccured() {
if currentDemand > 0 {
currentDemand += subscriber?.receive(control) ?? .none
currentDemand -= 1
}
}
}
}
In the initializer for this subscription object, I immediately call addTarget(_:action:for:)
on the control that’s passed to the initializer. This means that this subscription will immediately begin receiving updates for this UIControl even if the subscriber isn’t interested
in any events just yet. In this subscription’s request(_:) method I only increase the
currentDemand. We don’t send any values when the subscriber requests them because
the purpose of this subscriber is to send values when the specified UIControl.Event
occurs.
The cancel method on this subscription is pretty straightforward. I set the subscriber to
nil, and remove the subscription from the control’s targets.
The most interesting bit of this custom subscription is in eventOccured. In this method, the
subscriber’s receive method is called if it sent sufficient demand, and the current demand
is increased with the subscriber’s new demand and decreased by one because we just sent a
value to the subscriber.
Even though this custom publisher and subscription don’t contain a ton of code, it’s actually
a pretty advanced and complicated component that I’ve just shown you. Let’s look at the
publisher(for:) method now so you can see how this publisher is used:
extension UIControl {
func publisher(for event: UIControl.Event) ->
,→
UIControl.EventPublisher {
Donny Wals
173
Practical Combine
return UIControl.EventPublisher(control: self, controlEvent:
,→
event)
}
}
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
Donny Wals
174
Practical Combine
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.
Donny Wals
175
Practical Combine
Debugging your Combine code
Being able to write reactive code is a fantastic skill. An equally fantastic skill is being able to
debug your reactive code. Because Combine’s publishers are inherently asynchronous, it’s
not always easy to debug Combine code. In earlier chapters, I have shown you that you can
use Combine’s print operator to help you understand what your code does. In this chapter, I
will explain print a little bit more in-depth and I will show you how you can use a tool called
Timelane to help you understand your Combine code on a deeper level.
By the end of this chapter, you should be able to use print and Timelane with confidence to
help you gain insight into your code. I will use examples from earlier chapters to demonstrate
print and Combine. That way you should be familiar with the concepts I present while
gaining new insights that you may not have had otherwise.
Using print to debug your code
One of the simplest tools for debugging in any developer’s toolbox is the print statement.
Pretty much every language or SDK you can develop with has some way of printing values to
a console where you can inspect them to see whether your program produces the expected
outputs at the expected times. Combine provides its own print operator which prints
information about a publisher, it’s subscriptions and the values that the publisher emits to
Xcode’s console. In Chapter 3 - Transforming publishers I showed you the following code:
[1, 2, 3].publisher
.print()
.flatMap(maxPublishers: .max(1), { int in
return Array(repeating: int, count: 2).publisher
})
.sink(receiveValue: { value in
// handle value
})
This code produced the following output:
Donny Wals
176
Practical Combine
receive subscription: ([1, 2, 3])
request max: (1)
receive value: (1)
request max: (1)
receive value: (2)
request max: (1)
receive value: (3)
request max: (1)
receive finished
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.
Using Timelane to understand and debug
your code
While printing is a convenient way to do some quick and dirty debugging, there are often
better tools to understand and validate your code than printing tons of information to your
console. Timelane is a convenient open-source tool that was developed by Marin Todorov to
help developers understand and debug their Combine subscriptions in-depth. In this section,
Donny Wals
177
Practical Combine
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.
Preparing to use Timelane
To use Timelane, there are two things you need to do. First, you need to install the Instruments template that is used to visualize your data streams. Second, you need to add the
TimelaneCombine dependency to your project. Note that there is also a RxTimelane
framework available that allows you to use Timelane to profile RxSwift code. In this post, I
will focus on Combine but the RxSwift version works in the same manner as the Combine
version.
To install the Timelane Instruments template, go to the Timelane releases page on Github
and download the Timelane app zip file. Open the downloaded application and follow the
installation instructions shown in the app:
Donny Wals
178
Practical Combine
Figure 18: Screenshot of the Timelane installer app
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.
Donny Wals
179
Practical Combine
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
Donny Wals
180
Practical Combine
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 TimelaneCombine 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.
Using Timelane to debug a simple sequence
Once you have installed the Timelane Instruments template and added the TimelaneCombine dependency to your project, you can start using Timelane immediately using the lane
operator it provides. Let’s look at a simple example first. In Chapter 5 - Using Combine to
respond to user input, I introduced the debounce operator by showing you the following
code.
$searchQuery
.debounce(for: 0.3, scheduler: DispatchQueue.main)
Donny Wals
181
Practical Combine
.assign(to: \.text, on: label)
.store(in: &cancellables)
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:
Donny Wals
182
Practical Combine
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:
Donny Wals
183
Practical Combine
Figure 22: A screenshot of a profiled Timelane session
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:
Donny Wals
184
Practical Combine
$searchQuery
.lane("Raw search query")
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.lane("Search query")
.assign(to: \.text, on: label)
.store(in: &cancellables)
Performing the same actions results in the following Instruments output:
Figure 23: A screenshot of a Timelane session with two lanes
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
Donny Wals
185
Practical Combine
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 subscription 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:
Donny Wals
186
Practical Combine
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)
Donny Wals
187
Practical Combine
.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:
@Published var searchQuery: String = ""
You can replace it with Timelane’s @PublishedOnLane property as follows:
@PublishedOnLane("Search query") var searchQuery: String = ""
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:
@PublishedOnLane("Search query", filter: [.event]) var searchQuery:
,→
String = ""
Donny Wals
188
Practical Combine
Using Timelane to debug a sequence of publishers
Now that you understand how Timelane displays information in Instruments, and how you
can filter the data that is logged, let’s look at a more complex example of an Instruments
session that traces the complex networking sequence from Chapter 6 - Using Combine for
networking. Before I show you the relevant Instruments session I want to refresh your mind
and show you the diagram of the work that was done:
Figure 25: The sequence of complex tasks from Chapter 6
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:
Donny Wals
189
Practical Combine
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:
Donny Wals
190
Practical Combine
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 publisher 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 publisher 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.
Donny Wals
191
Practical Combine
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.
Donny Wals
192
Practical Combine
Testing code that uses Combine
An important skill to have as a software developer is the ability to write good unit tests. A
good test suite helps you verify that your code works as expected and ideally your test suite
also ensures that your code can handle edge cases without trouble.
Writing a good test suite is not an easy task though. You can’t just take any codebase and write
a good test suite for it. You need to carefully craft your codebase to be testable by creating the
right abstractions in the right places. And even if you get this balancing act right, it’s not always
easy to write tests. Especially if the code you’re writing tests for runs asynchronously.
Combine code runs asynchronously by its very nature, which makes it non-trivial to test. If
you’re familiar with unit testing, it’s only logical that you may have been wondering about
tests throughout reading this book. In this chapter, I will show you how you can test your
Combine code. You will learn to do this through the following topics:
• Writing tests for code that uses Combine
• Optimizing Combine code for testability
• Creating helpers to make testing Combine code easier
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.
Donny Wals
193
Practical Combine
Writing tests for code that uses Combine
When you’re writing code that you plan to write tests for, it’s important that you make sure
that you can mock, fake or stub the parts of your code that are not essential to your test.
This means that you should at least make sure that you don’t rely on a network connection,
databases, the file system or any other external source that you don’t explicitly control during
your unit tests.
Relying on external sources will make your test suite unreliable and prone to mistakes. After
all, you don’t want your unit test to fail because another test manipulated the database in a
way that your current test did not expect. If that happens your unit test would make it look like
your code is flawed while in reality your code didn’t receive the input it expected. Or possibly
worse, what if your code is flawed but it’s hidden because the test that happens to run before
the test that tests your flawed code happens to set up the environment a way that favors your
faulty code. Your test would pass as long as the tests run in the correct order and your bug
could go unnoticed for much longer than you should be comfortable with.
So how does testing work with Combine? To figure that out, it’s important to have a baseline
understanding of what we should, and should not test. Let’s look at the Car model I introduced
in Chapter 4 - Updating the User Interface:
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
Donny Wals
194
Practical Combine
kwhInBattery is marked as @Published, we should be able to assume that everything
works as expected.
However, I still want to write the test I just mentioned because it also serves as a form of
documentation and we could remove the @Published annotation at any time if we don’t
verify that we can subscribe to $kwhInBattery.
By writing a test that subscribes to $kwhInBattery and validates its output, we formalize
that kwhInBattery is @Published rather than testing whether @Published works as
expected.
Let’s see how you could set up a test that validates that no side-effects are applied to kwhInBattery when we assign it, and a test that validates that we get the expected values in our
sink. The following code sets up the skeleton for the test:
class CarTest: XCTestCase {
var car: Car!
var cancellables: Set<AnyCancellable>!
override func setUp() {
car = Car()
cancellables = []
}
func testKwhBatteryIsPublisher() {
let newValue: Double = 10.0
var expectedValues = [car.kwhInBattery, newValue]
let receivedAllValues = expectation(description: "all values
,→
received")
car.$kwhInBattery.sink(receiveValue: { value in
// we'll write the assertion logic here
}).store(in: &cancellables)
car.kwhInBattery = newValue
Donny Wals
195
Practical Combine
waitForExpectations(timeout: 1, handler: nil)
}
}
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 testKwhBatteryIsPublisher 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 waitForExpectations(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
Donny Wals
let expectedValue = expectedValues.first else {
196
Practical Combine
XCTFail("The publisher emitted more values than expected.")
return
}
guard expectedValue == value else {
XCTFail("Expected received value \(value) to match first expected
,→
value \(expectedValue)")
return
}
// 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
Donny Wals
197
Practical Combine
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
lazy var batterySubject: AnyPublisher<String?, Never> = {
return car.$kwhInBattery.map({ newCharge in
return "The car now has \(newCharge)kwh in its battery"
}).eraseToAnyPublisher()
}()
mutating func drive(kilometers: Double) {
let kwhNeeded = kilometers * car.kwhPerKilometer
assert(kwhNeeded <= car.kwhInBattery, "Can't make trip, not
,→
enough charge in battery")
car.kwhInBattery -= kwhNeeded
}
}
Notice how batterySubject creates a publisher that converts the Double values published 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:
Donny Wals
198
Practical Combine
class CarViewModelTest: XCTestCase {
var car: Car!
var carViewModel: CarViewModel!
var cancellables: Set<AnyCancellable>!
override func setUp() {
car = Car()
carViewModel = CarViewModel(car: car)
cancellables = []
}
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"
}
let receivedAllValues = expectation(description: "all values
,→
received")
carViewModel.batterySubject.sink(receiveValue: { value in
//we'll write the assertion logic here
}).store(in: &cancellables)
carViewModel.drive(kilometers: 10)
waitForExpectations(timeout: 1, handler: nil)
}
}
Notice how similar this test looks to the previous test.
A big difference is that I’ve changed my newValue to be car.kwhInBattery car.kwhPerKilometer * 10. The reason for this is that I want to call drive on the
Donny Wals
199
Practical Combine
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 expectation 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 implementation 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
}
guard
let expectedValue = expectedValues.first else {
XCTFail("The publisher emitted more values than expected.")
return
}
guard expectedValue == value else {
XCTFail("Expected received value \(value) to match first expected
,→
value \(expectedValue)")
return
}
expectedValues = Array(expectedValues.dropFirst())
Donny Wals
200
Practical Combine
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.
Optimizing Combine code for testability
Writing a good test suite requires you to write testable code. If your code was not designed
with testability in mind you’ll likely end up with a poorly written test suite that produces
questionable results.
I have already mentioned that you should design your code in a way that allows you to replace,
or mock certain objects. This wasn’t needed for the tests that I showed you earlier because
the code was fairly trivial and didn’t rely on any external resources.
But how can you optimize Combine code that does rely on external resources to be tested
properly?
The answer isn’t any more exciting than it would be for other code. Optimizing your Combine
code to be testable means that you abstract external resources, or dependencies, behind
protocols so you can inject special versions of these dependencies in your tests.
In this section, I will demonstrate this using two examples. First, I will show you how you can
make code that relies on NotificationCenter notifications testable. The second example
will be slightly more complicated because it involves writing a protocol to abstract (and test)
an object that relies on the network.
Donny Wals
201
Practical Combine
Architecting code that uses a NotificationCenter publisher
In Chapter 2 - Exploring publishers and subscribers I first showed you how you
can subscribe to Notification Center events in Combine using a NotificationCenter.Publisher. Listening to Notification Center events can be useful for a plethora of
reasons.
In your UI, you might want to listen for notifications that tell you whether the keyboard is shown
or hidden. In an application that uses Core Data, you might want to listen for notifications that
signal that a managed object context was updated. Or maybe your app uses an in-memory
cache for data that is expensive to calculate and you’re using NotificationCenter to
listen for memory warnings so you can purge your cache when the system tells you it’s low on
memory.
With Combine, it’s easy to subscribe to events that are published through NotificationCenter because Apple has implemented a convenient publisher for us. But that’s not what I
want to explain in this section. Instead, I want you to examine the following code and think
about how you would test it:
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.
Donny Wals
202
Practical Combine
If you’ve given some thought to how you would test this code you may have come up with an
idea similar to this:
•
•
•
•
Set up an instance of ImageLoader
Add a couple of images to the images dictionary
Send a memory warning notification through NotificationCenter.default
Check that the cached image is no longer cached after sending a memory warning
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 behavior.
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 NotificationCenter.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 ImageLoader as follows:
class ImageLoader {
var images = [URL: UIImage]()
var cancellables = Set<AnyCancellable>()
init(_ notificationCenter: NotificationCenter =
NotificationCenter.default) {
,→
let notification =
,→
UIApplication.didReceiveMemoryWarningNotification
notificationCenter.publisher(for: notification)
Donny Wals
203
Practical Combine
.sink(receiveValue: { [weak self] _ in
self?.images = [URL: UIImage]()
}).store(in: &cancellables)
}
}
I haven’t changed much. All I did was allow users of ImageLoader to inject a specific NotificationCenter 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 without 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
class ImageLoaderTests: XCTestCase {
func testImageLoaderClearsImagesOnMemoryWarning() {
// setup
let notificationCenter = NotificationCenter()
let imageLoader = ImageLoader(notificationCenter)
// store a dummy image
let image = UIImage(systemName: "house")
let url = URL(string: "https://fake.url/house")!
imageLoader.images[url] = image
XCTAssertNotNil(imageLoader.images[url])
// send memory warning
Donny Wals
204
Practical Combine
let memoryWarning =
,→
UIApplication.didReceiveMemoryWarningNotification
notificationCenter.post(Notification(name: memoryWarning))
// verify that the images are now gone
XCTAssertNil(imageLoader.images[url])
}
}
The test is fairly straightforward. I create an ImageLoader and NotificationCenter
object, store a UIImage in it using a made-up URL and check whether the image is actually
available in the ImageLoader’s images dictionary. It’s okay that the URL is made-up and
that the UIImage that I store is just an SF Symbol image. I’m not testing whether I can load
images from arbitrary locations. I’m only testing whether ImageLoader clears any images
that it stored when a memory warning is received.
After performing all the required setup I post a memory warning to my custom Notification
Center object and lastly I verify that the images dictionary no longer contains an entry for the
URL I created earlier.
I hope that at this point you are wondering where the Combine is in this test.
We don’t use Combine in this test at all because we don’t have to. We’re using Combine in the
implementation of ImageLoader but that doesn’t mean that we need to use Combine in
the test that verifies whether ImageLoader works as expected.
Keep in mind that we’re not testing Combine. That’s Apple’s job. Our job is to test that our
code is written properly and works as expected.
Ultimately, the fact that Combine is used to listen for memory warnings is an implementation
detail. That doesn’t mean we need to use Combine to verify that the implementation is
correct.
The next example builds upon this ImageLoader and adds some testable networking capabilities to it.
Donny Wals
205
Practical Combine
Architecting code that requires a networking layer
So far the tests you have written are fairly trivial. In this section, I want to show you a somewhat
more complicated setup that, while more complex, is built upon the exact same rules and
foundation as the code you wrote in the previous subsection.
Since the ImageLoader is fairly useless without the ability to load images from some kind
of external source, I would like to add some networking capabilities to it.
The simplest way to achieve this would be to add the following method to ImageLoader:
func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> {
if let image = images[url] {
return Just(image).setFailureType(to:
Error.self).eraseToAnyPublisher()
,→
}
return URLSession.shared.dataTaskPublisher(for: url)
.mapError({ $0 as Error })
.tryMap({ response in
guard let image = UIImage(data: response.data) else {
throw ImageLoaderError.invalidData
}
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
Donny Wals
206
Practical Combine
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 operations 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 URLSession.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 ImageNetworkProvider: ImageNetworking {
func loadURL(_ url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.mapError({$0 as Error})
.map(\.data)
.eraseToAnyPublisher()
}
}
It’s now possible to refactor ImageLoader to require an ImageNetworking object in its
initializer, similar to how it requires a NotificationCenter. The only difference is that
Donny Wals
207
Practical Combine
we also need an instance variable on ImageLoader to hold on to the ImageNetworking
object:
class ImageLoader {
var images = [URL: UIImage]()
var cancellables = Set<AnyCancellable>()
let network: ImageNetworking
init(_ notificationCenter: NotificationCenter =
NotificationCenter.default,
,→
network: ImageNetworking = ImageNetworkProvider()) {
self.network = network
// Notification center related code
}
func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> {
// This will be updated soon
}
}
Okay, at this point the ImageLoader depends on an object that implements the ImageNetworking protocol. By default we’ll use an instance of ImageNetworkProvider
but we can use a different object when testing the ImageLoader. Let’s update loadImage(at:) so it uses this new ImageNetworking object:
func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> {
if let image = images[url] {
return Just(image).setFailureType(to:
,→
Error.self).eraseToAnyPublisher()
}
return network.loadURL(url)
Donny Wals
208
Practical Combine
.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 images 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.
class MockImageNetworkProvider: ImageNetworking {
var wasLoadURLCalled = false
func loadURL(_ url: URL) -> AnyPublisher<Data, Error> {
wasLoadURLCalled = true
let data = UIImage(systemName: "house")!.pngData()!
Donny Wals
209
Practical Combine
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:
var cancellables = Set<AnyCancellable>()
override func setUp() {
cancellables = Set<AnyCancellable>()
}
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)
Donny Wals
210
Practical Combine
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")
,→
// load the image
imageLoader.loadImage(at: url).sink(receiveCompletion: { completion
,→
in
guard case .finished = completion else {
XCTFail("Expected load to complete succesfully")
return
}
// verify our assumptions
XCTAssertEqual(mockNetwork.wasLoadURLCalled, true)
XCTAssertNotNil(imageLoader.images[url])
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
// wait for expectations
waitForExpectations(timeout: 1.0, handler: nil)
}
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.
Donny Wals
211
Practical Combine
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.
Donny Wals
212
Practical Combine
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:
.handleEvents(receiveOutput: { [weak self] image in
self?.images[url] = image
})
Here’s the handleEvents operator in the full context of loadImage(at:):
func loadImage(at url: URL) -> AnyPublisher<UIImage, Error> {
if let image = images[url] {
return Just(image).setFailureType(to:
Error.self).eraseToAnyPublisher()
,→
}
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
Donny Wals
213
Practical Combine
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")!
// populate the cache
imageLoader.images[url] = UIImage(systemName: "house")
// expectations
let loadCompleted = expectation(description: "expected image load
to complete")
,→
let imageReceived = expectation(description: "expected to receive
an image")
,→
// load the image
imageLoader.loadImage(at: url).sink(receiveCompletion: { completion
,→
in
guard case .finished = completion else {
XCTFail("Expected load to complete succesfully")
return
}
// verify our assumptions
XCTAssertEqual(mockNetwork.wasLoadURLCalled, false)
XCTAssertNotNil(imageLoader.images[url])
Donny Wals
214
Practical Combine
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
// wait for expectations
waitForExpectations(timeout: 1.0, handler: nil)
}
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 receiveCompletion 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 NotificationCenter 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.
Donny Wals
215
Practical Combine
Creating helpers to make testing
Combine code easier
By now you should have an idea of you can test Combine code. If you’re new to testing I
understand that you probably have many questions right now. If that’s the case, I would like
to point you to the testing section on my blog. There you can brush up on your testing basics
in a Combine-less context. After that, you can come back to this chapter and give it another
read. I’m sure it will make more sense by then. Testing is one of those things that you could
write stacks of books about so don’t feel discouraged if you don’t feel like a testing expert just
yet. It takes time and practice to get better at testing.
In this section, I would like to revisit some of the tests I have shown you in the previous section,
and show you two neat helper methods that you could implement to improve the test suite.
First up is the CarViewModelTest:
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"
}
let receivedAllValues = expectation(description: "all values
received")
,→
carViewModel.batterySubject.sink(receiveValue: { value in
guard let value = value else {
XCTFail("Expected value to be non-nil")
return
}
guard
let expectedValue = expectedValues.first else {
XCTFail("The publisher emitted more values than expected.")
return
}
Donny Wals
216
Practical Combine
guard expectedValue == value else {
XCTFail("Expected received value \(value) to match first
,→
expected value \(expectedValue)")
return
}
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
receivedAllValues.fulfill()
}
}).store(in: &cancellables)
carViewModel.drive(kilometers: 10)
waitForExpectations(timeout: 1, handler: nil)
}
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
Donny Wals
217
Practical Combine
return "The car now has \(doubleValue)kwh in its battery"
}
let receivedAllValues = expectation(description: "all values
,→
received")
carViewModel.batterySubject
.assertOutput(matches: expectedValues, expectation:
receivedAllValues)
,→
.store(in: &cancellables)
carViewModel.drive(kilometers: 10)
waitForExpectations(timeout: 1, handler: nil)
}
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:
extension Publisher where Output: Equatable {
func assertOutput(matches: [Output], expectation:
,→
XCTestExpectation) -> AnyCancellable {
var expectedValues = matches
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
}
guard expectedValue == value else {
Donny Wals
218
Practical Combine
XCTFail("Expected received value \(value) to match first
,→
expected value \(expectedValue)")
return
}
expectedValues = Array(expectedValues.dropFirst())
if expectedValues.isEmpty {
expectation.fulfill()
}
})
}
}
I added assertOutput as an extension on Publisher so it can be used as an operator. It
also returns an AnyCancellable so the caller of assertOutput can keep the subscription alive for as long as needed.
The implementation of assertOutput is fairly straightforward. I create a mutable copy of
the matches array by assigning it to a variable and I subscribe to the publisher assertOutput is called on. This is self. I then compare all emitted values using the same logic I used
before and eventually I fulfill the expectation, just like the code did when it was still part
of CarViewModelTest. But by making this logic available as an operator on Publisher
it’s now possible to quickly apply this logic where needed.
Try refactoring CarTest to use this operator. You should be able to do this by looking at the
updated test I just showed you.
You can use assertOutput to compare any publisher’s output with an array of expected
values as long as the publisher’s output is Equatable. Note that this helper does not concern
itself with a completion event. The reason is that this helper can be used on publishers that typically don’t ever complete like @Published or the NotificationCenter.Publisher.
The second test I would like to refactor with you is the ImageLoader test. In this test the
receiveCompletion closure was used to perform a couple of assertions. In other words,
we weren’t interested in validating the publisher’s output as much as we were interested in
validating that the publisher did the right thing. Let’s look at one of the tests I showed you in
Donny Wals
219
Practical Combine
the previous section once more:
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")
,→
// load the image
imageLoader.loadImage(at: url).sink(receiveCompletion: { completion
,→
in
guard case .finished = completion else {
XCTFail("Expected load to complete succesfully")
return
}
// verify our assumptions
XCTAssertEqual(mockNetwork.wasLoadURLCalled, true)
XCTAssertNotNil(imageLoader.images[url])
loadCompleted.fulfill()
}, receiveValue: { image in
// acknowledge that we received an image
imageReceived.fulfill()
}).store(in: &cancellables)
// wait for expectations
waitForExpectations(timeout: 1.0, handler: nil)
Donny Wals
220
Practical Combine
}
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")!
let result = awaitCompletion(for: imageLoader.loadImage(at: url))
XCTAssertNoThrow(try result.get())
XCTAssertEqual(mockNetwork.wasLoadURLCalled, true)
XCTAssertNotNil(imageLoader.images[url])
}
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:
Donny Wals
221
Practical Combine
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)
})
waitForExpectations(timeout: 1.0, handler: nil)
return result
}
}
This helper is defined on XCTestCase and it uses the XCTestExpectation feature to
synchronously return a Result object. The awaitCompletion(for:) method is generic
over P which should be a Publisher. It returns a Result<[P.Output], P.Failure>.
In other words, it returns a result object that either holds an array of emitted values or an
error.
To obtain the result object I subscribe to the passed publisher and collect its output in the
output array. When the publisher’s receiveCompletion is called I create the Result
object and call fullfil on the finishedExpectation. At that point, the code will continue running past the line where I call waitForExpectations and the Result object
Donny Wals
222
Practical Combine
is returned. Remember that waitForExpectations halts the execution of your code.
This means that you can use it to wait for a publisher to complete before returning from
awaitCompletion. Pretty nifty I’d say.
Note that this helper is only useful for publishers that you expect to complete. If you don’t
expect your publisher to complete you’re better off using the assertOutput operator that
I showed you earlier in this section.
Try refactoring the other ImageLoader test that you wrote in the previous section using
waitForExpectations on your own if you’re coding along. I’m sure you can do it.
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.
Donny Wals
223
Practical Combine
Driving publishers and flows with
subjects
Up until this point, I have shown you relatively simple publishers and examples. They sometimes used complicated operators, or chained operators together in surprising ways but
overall they were relatively straightforward to build and understand. That doesn’t mean they
weren’t complex when you first saw them, but as you write more and more Combine code,
you’ll realize that the principles you’ve seen up to this point aren’t that scary.
The problems we’ve solved so far weren’t very new or necessarily complex either. That’ll
change in this chapter.
In this chapter, I want to take a closer look at CurrentValueSubject and PassthroughSubject and show you how you can use these subjects together with a strategically placed
flatMap to build really cool and complicated interactions and flows in your applications.
By the end of this chapter you will have an idea of how you can work with paginated network
calls, fetch partial data, and retrieve data recursively. I will also present an alternative implementation for the token refresh flow that I showed you in Chapter 6 - Using Combine for
networking.
Retrieving partial data in Combine
When working with large amounts of data it might be beneficial to load parts of your data
instead of loading it all at once. This is common in social media applications that load more
and more data as you scroll down an endless feed of pictures, videos, and text. Batched
loading is also used when processing data or reading a large number of files. It wouldn’t make
sense to load a heap of files into memory if you can only process one file at a time.
Regardless of the exact data your loading, the principle of retrieving partial data is almost
always the same. You request a section of data, process it, and at a later time you request the
next section of data. The trigger for this request could be user interaction, or anything else.
An object that’s used to retrieve such partial data that doesn’t use Combine might look a bit
like this:
Donny Wals
224
Practical Combine
class DataFetcher {
private var currentPage = 0
func loadNextPage(_ completion: @escaping (Result<Data, Error>) ->
,→
Void) {
// perform load
// increment currentPage
// call completion handler
}
}
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
var dataPublisher = PassthroughSubject<Data, URLError>()
var cancellables = Set<AnyCancellable>()
Donny Wals
225
Practical Combine
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
Donny Wals
226
Practical Combine
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 URLSession.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
lazy var dataPublisher: AnyPublisher<Data, URLError> = {
loadRequestPublisher
.flatMap({ [weak self] _ -> URLSession.DataTaskPublisher in
let url = URL(string:
,→
"https://practicalcombine.com?page=\(self?.currentPage ??
,→
0)")!
return URLSession.shared.dataTaskPublisher(for: url)
})
.map(\.data)
.eraseToAnyPublisher()
}()
private var loadRequestPublisher = PassthroughSubject<Void,
,→
Never>()
func loadNextPage() {
loadRequestPublisher.send(())
}
}
Donny Wals
227
Practical Combine
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 emitted 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 URLSession.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:
lazy var dataPublisher: AnyPublisher<Data, URLError> = {
loadRequestPublisher
.flatMap({ [weak self] _ -> URLSession.DataTaskPublisher in
let url = URL(string:
,→
"https://practicalcombine.com?page=\(self?.currentPage ??
,→
0)")!
return URLSession.shared.dataTaskPublisher(for: url)
})
.map(\.data)
.handleEvents(receiveOutput: { [weak self] _ in
self?.currentPage += 1
Donny Wals
228
Practical Combine
})
.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.
Recursively loading data with subjects
and flatMap
We can take the ideas from the previous section and apply them in a different way to build
a function that will recursively load pages from a remote endpoint until there are no more
pages to load. Paginated responses, or paginated APIs, come in many shapes. In this section
I’m going to assume that a server responds with a very simple object that has the following
shape:
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:
Donny Wals
229
Practical Combine
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
func loadAllPages() -> AnyPublisher<[String], Never> {
}
}
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
Donny Wals
230
Practical Combine
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:
func loadAllPages() -> AnyPublisher<[String], Never> {
let pageIndexPublisher = CurrentValueSubject<Int, Never>(0)
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
Donny Wals
231
Practical Combine
current page as its current value.
This is really neat because a CurrentValueSubject emits its current value when it receives a subscriber.
In other words, when we subscribe to the publisher returned by loadAllPages, the
pageIndexPublisher will receive a subscriber and emit its initial value. This value is
then flatMapped over to create a new publisher. This new publisher is created by calling
network.loadPage(_:) and passing the emitted pageIndex to it. At this point, we
have a publisher that emits ApiResponse objects since that’s what the publisher created in
network.loadPage(_:) emits.
In the next operator, I use handleEvents to inspect the emitted response to see if
the response’s hasMorePages property is true. If it is, I increment pageIndexPublisher.value by one. This will make it emit a new value, which will be sent through my
flatMap, and kick off a new page load. If there are no more pages to load I complete the
pageIndexPublisher by calling send(completion: .finished) on it.
After handleEvents, we enter the reduce operator. This operator will capture all emitted
ApiResponse values and combine them into a single array of String objects. The reduce
operator takes an initial value (an empty array of String objects) and a closure that is called
every time a value is emitted by the upstream publisher. In this closure, I return the combined
value of the existing array of strings and the newly fetched strings.
When the pageIndexPublisher completes, my String array is emitted by reduce and
the subscriber of loadAllPages() will receive a single array with all strings that were
loaded.
Let’s see how this RecursiveFetcher is used:
let fetcher = RecursiveFetcher(network: NetworkObject())
fetcher.loadAllPages()
.sink(receiveValue: { strings in
print(strings)
})
.store(in: &cancellables)
The output of this code is:
Donny Wals
232
Practical Combine
["Item", "Item", "Item", "Item", "Item", "Item", "Item", "Item",
,→
"Item", "Item", "Item", "Item"]
Pretty cool, right?
We were able to drive a recursive network call using a CurrentValueSubject combined
with a flatMap, handleEvents and a reduce due to the fact that a CurrentValueSubject emits a value when it receives a subscriber, which is sent to flatMap and kicking off a
network call. The handleEvents operator then checks whether the CurrentValueSubject should complete or emit another value, and finally the reduce collects and combines
all emitted values into a single value that’s emitted when the CurrentValueSubject
completes.
This is pretty advanced stuff, but I love how we can build complicated features using small,
simple building blocks as long as we find and follow the appropriate patterns.
Let’s look at one more cool example of using a Subject together with flatMap to drive a
complex flow.
Automatically retrying a network call
after a token refresh
In Chapter 6 - Using Combine for networking, I showed you a simple version of a token
refresh flow that would retry a network request after attempting a token refresh. In this
section, I will show a more sophisticated approach that, in my opinion, looks nicer than the
simpler version.
Neither is inherently better in terms of functionality, I just think the approach in this chapter
is a bit cleaner.
Since you already know how to do networking in Combine, I will jump right in and show you
the starting point of this section’s feature:
Donny Wals
233
Practical Combine
struct Token {
let isValid: Bool
}
class Authenticator {
private var currentToken = Token(isValid: false)
func refreshToken<S: Subject>(using subject: S) where S.Output ==
,→
Token {
self.currentToken = Token(isValid: true)
subject.send(currentToken)
}
func tokenSubject() -> CurrentValueSubject<Token, Never> {
return CurrentValueSubject(currentToken)
}
}
struct UserApi {
let authenticator: Authenticator
func getProfile() -> AnyPublisher<Data, URLError> {
}
}
The Token object is a simple object that I defined to easily fake an expired token. The Authenticator 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.
Donny Wals
234
Practical Combine
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 until the pipeline is finished. First, we’ll need a subject that drives the pipeline by emitting an initial token. The CurrentValueSubject created by Authenticator.tokenSubject
is a perfect fit for this:
func getProfile() -> AnyPublisher<Data, URLError> {
let tokenSubject = authenticator.tokenSubject()
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 network call whenever we receive a token:
func getProfile() -> AnyPublisher<Data, URLError> {
let tokenSubject = authenticator.tokenSubject()
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
return URLSession.shared.dataTaskPublisher(for: url)
.eraseToAnyPublisher()
})
Donny Wals
235
Practical Combine
.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 URLSession.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:
func getProfile() -> AnyPublisher<Data, URLError> {
let tokenSubject = authenticator.tokenSubject()
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
return URLSession.shared.dataTaskPublisher(for: url)
.flatMap({ result -> AnyPublisher<Data, URLError> in
if let httpResponse = result.response as? HTTPURLResponse,
httpResponse.statusCode == 403 {
self.authenticator.refreshToken(using: tokenSubject)
return Empty().eraseToAnyPublisher()
}
Donny Wals
236
Practical Combine
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 getProfile():
func getProfile() -> AnyPublisher<Data, URLError> {
let tokenSubject = authenticator.tokenSubject()
Donny Wals
237
Practical Combine
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "https://www.donnywals.com")!
return URLSession.shared.dataTaskPublisher(for: url)
.flatMap({ result -> AnyPublisher<Data, URLError> in
if let httpResponse = result.response as? HTTPURLResponse,
httpResponse.statusCode == 403 {
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 immediately. 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!
Donny Wals
238
Practical Combine
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!
Donny Wals
239
Practical Combine
Where to go from here?
You’ve read this book and you already know a ton about Combine. However, I have only
helped you scratch the surface of Combine and what you can do with it. In this short chapter, I
would like to recommend some useful tools and resources to you that you might want to pick
up or begin following to help you on your journey to fully master and implement Combine.
First and foremost, I would like to recommend the amazing free website/book built by Joseph
Heck. He put together an amazing online Combine reference that I have definitely used more
than a couple of times while I was exploring Combine myself. You can find his website here:
https://heckj.github.io/swiftui-notes/. If you enjoy this resource, make sure to grab the
epub version on Gumroad: http://gumroad.com/l/usingcombine. It helps Josepth to
maintain and improve his Combine content and he simply deserves all the support he can
get.
A very good resource written by an extremely active member of the Combine community
is https://www.apeth.com/UnderstandingCombine/ which is created by Matt Neuburg.
This website contains a ton of information on advanced and complicated Combine concepts.
Moreover, if you ever ask a question on the Swift forums or stackoverflow, there’s a reasonable
chance that Matt will jump in and help you!
Normally I would link to Apple’s documentation on a topic I wrote about to help you explore
more but unfortunately, Apple’s Combine documentation isn’t fantastic. It’s worth taking a
look if you’re interested but I don’t think you’ll find anything that’s amazingly insightful or
helpful there. I know I’ve been underwhelmed while writing this book a couple of times. That
said, I hope Apple improves their documentation soon so I can update this paragraph with
some kind words and a reference to Apple’s docs.
If the final chapter of this book left you a little bit overwhelmed and you want to learn more
about what I tend to call “The Combine Triad”, you should take a look at the talk I gave at
dotSwift in February of 2020: https://www.dotconferences.com/2020/02/donny-walsthe-combine-triad.
Reactive and functional programming are interesting topics and I think it’s absolutely worth
it for you to take deep dives into these topics if you enjoyed this book. Daniel Steinberg
has produced a lot of interesting content that involves functional programming over the
years and I absolutely recommend that you look him up on YouTube and in particular his talk
Donny Wals
240
Practical Combine
Understanding Combine from Pragma conf 2019.
All in all, you’re ready to start refactoring your codebase to integrate Combine. Try to start
with small sections of your app, or maybe a part of your code that is nice and isolated from
the rest like a networking layer. Work your way from there towards integrating Combine with
your UI. This approach should work regardless of the UI framework you’re using.
The best way to learn is to do, and with all the information from this book and the resources
I’ve shared in this chapter, nothing is stopping you from achieving great results with Combine.
Sure, you’ll get stuck sometimes, you’ll be confused and lost but I’m sure you’ll come out of it
with more knowledge every single time.
I’d like to take this moment to thank you for getting and reading my book one last time, I truly
appreciate it and it means a lot to me. Don’t hesitate to send me any questions or feedback
you have via an email to feedback@donnywals.com. I love to hear what you thought of this
book, and I’d love to know how you’re integrating Combine in the projects you’re working
on.
Donny Wals
241
Download
Study collections