Uploaded by spakslow

[Ebook] BigMountainStudio - Combine Mastery in SwiftUI

advertisement
VISUAL TIME-SAVING REFERENCE
Combine Mastery
iOS 15
In SwiftUI
Mark Moeykens
www.bigmountainstudio.com
1 FOR SWIFTUI DEVELOPERS
A COMBINE REFERENCE GUIDE
Combine MasteryStudio
in SwiftUI
Big Mountain
Version: 30-DECEMBER-2021
©2021 Big Mountain Studio LLC - All Rights Reserved
www.bigmountainstudio.com
2
Combine Mastery in SwiftUI
ACKNOWLEDGMENTS
Writing a book while also working a full-time job takes time away from
family and friends. So first of all, I would like to thank my wife Jaqueline and
daughter Paola for their patience and support.
Next, I would like to thank my patrons for their feedback, especially:
Stewart Lynch, Chris Parker, Basil, Herman Vermeulen, Franklin
Byaruhanga, Paul Colton for coming up with the first pipeline example, Jim
Fetters, Mariusz Bohdanowicz, Ronnie Pitman, Marlon Simons, Emin Grbo,
and Rob In der Maur.
I would also like to thank my friends who always gave me constant
feedback, support, and business guidance: Chris Ching, Scott Smith, Rod
Liberal, Chase Blumenthal and Chris Durtschi.
I would also like to thank the Utah developer community for their help in
making this book possible. This includes Dave DeLong, Parker Wightman,
Dave Nutter, Lem Guerrero, Chris Evans, and BJ Homer.
Many other developers also proof-read and gave feedback on the book.
These include: Tim Barrett, Florian Schweizer, Chaithra Pabbathi, Ron
Avitzur, Mariusz, Udin Rajkarnikar, Jeff Deimund, Steve Zhou, Shane Miller,
Thomas Swatland, Nadheer Chatharoo, Marco Mayen (Kross), Pushpinder
Pal Singh, Mats Braa, Eric, Schofield, Stanislav Kasprik, Sev Moreno Breser,
Mahmoud Ashraf, Sebastian Vidrea, Peter Pohlmann, Erica Gutierrez,
Stephen Zyszkiewicz, Alireza Toghyiani, David Hosier, and Luke Smith.
And finally, I would like to thank the creators of all the other sources of
information, whether Swift or Combine, that really helped me out and
enabled me to write this book. That includes Apple and their
documentation and definition files, Shai Mishali, Marin Todorov, Donny
Wals, Karin Prater, Antoine van der Lee, Paul Hudson, Joseph Heck, Vadim
Bulavin, Daniel Steinberg and Meng To.
TABLE OF CONTENTS
The table of contents should be built into your ePub and PDF readers. Examples:
Books App
Adobe Acrobat Reader
Preview
BOOK CONVENTIONS
Using iOS
I will use SwiftUI in iOS for examples because the screen shots will be smaller, the audience is bigger, and, well, that’s what I’m more familiar with too.
Using iOS
Template
TEMPLATE
I am using a custom view to format the title (1), subtitle (2), and descriptions (3) for the examples
in this book.
1
2
3
The following pages contain the custom code that you should include in your project if you will
be copying code from this book. (You can also get this code from the companion project too.)
See next page for
this code.
struct Using_iOS: View {
var body: some View {
VStack(spacing: 20) {
Use width: 214
HeaderView("Using iOS",
subtitle: "Introduction",
desc: "Let's use iOS as the view that will consume the data.")
Text("<Insert example here>")
Spacer()
}
.font(.title)
}
}
www.bigmountainstudio.com
7
Combine Mastery in SwiftUI
Using iOS
Template Code
struct HeaderView: View {
struct DescView: View {
var title = "Title"
var desc = "Use this to..."
var subtitle = "Subtitle"
var desc = "Use this to..."
init(_ desc: String) {
self.desc = desc
init(_ title: String, subtitle: String, desc: String) {
}
self.title = title
self.subtitle = subtitle
var body: some View {
self.desc = desc
Text(desc)
}
.frame(maxWidth: .infinity)
3
var body: some View {
VStack(spacing: 15) {
.background(Color("Gold"))
.foregroundColor(.white)
if !title.isEmpty {
}
Text(title)
.font(.largeTitle)
.padding()
}
1
}
Text(subtitle)
2
.foregroundColor(.gray)
DescView(desc)
}
}
}
www.bigmountainstudio.com
8
Combine Mastery in SwiftUI
Using iOS
Custom Xcode Editor Theme
I created a code editor color theme for a high-contrast light mode. This is the theme I use for the code throughout this book.
If you like this color theme and would like to use it in your Xcode then you can find it on my GitHub as a gist here.
Note
If you download the theme from the gist, look at the
first line (comment) for where to put it so Xcode
can see it.
www.bigmountainstudio.com
9
Combine Mastery in SwiftUI
􀎷
Using iOS
Embedded Videos
The ePUB version of the book supports embedded videos.
The PDF version does not.
This icon indicates that this is a playable video
in the ePUB format.
But in PDF it renders as simply a screenshot.
Note: In some ePUB readers, including Apple Books,
you might have to tap TWICE (2) to play the video.
www.bigmountainstudio.com
10
Combine Mastery in SwiftUI
Architecture for Examples
Model
ViewModel
View
When I teach a Combine concept, I want you to see the entire flow from start to end. From the Combine part to the SwiftUI view part.
To do this I will use a condensed variation of the Model - View - View Model (MVVM) architecture to connect data to the screen. I’ll show you what I call each part
and how I use it in the book to present code examples to you.
Note: I know each of these parts can be called and mean different things to many different developers. The goal here is just to let you know how I separate out the examples
from the view so you know what’s going on. This isn’t a book about architecture and I’m not here to debate what goes where and what it should be called.
Quick Overview of Architecture
Here is a quick overview of the architecture this book will be using. If this is new for you, keep reading as I discuss each part on the following pages.
(Note, it may not be exactly as you learned it or as someone else taught it. But I want to lay it out here so you know the convention you’ll be seeing.)
Model
View Model
struct BookModel: Identifiable {
var id = UUID()
var name = ""
}
View
class BookViewModel: ObservableObject {
@Published var books = [BookModel]()
func fetch() {
books =
[BookModel(name:
BookModel(name:
BookModel(name:
BookModel(name:
}
"SwiftUI
"SwiftUI
"Data in
"Combine
struct BookListView: View {
@StateObject var vm = BookViewModel()
var body: some View {
List(vm.books) { book in
HStack {
Image(systemName: "book")
Text(book.name)
}
}
.onAppear {
vm.fetch()
}
}
Views"),
Animations"),
SwiftUI"),
Reference")]
}
}
[
,
,
,
12
􀉚
􀉚
􀉚
www.bigmountainstudio.com
􀉚
􀉚
Architecture
]
Combine Mastery in SwiftUI
􀉚
Architecture
Model
struct BookModel: Identifiable {
var id = UUID()
var name = ""
}
I use the Model to hold all the data needed to represent one thing.
This model is conforming to the Identifiable protocol by implementing a property for id. This will help
the view when it comes time to display the information.
Keep in mind that architecture and naming is something where you’ll get 12 different opinions from 10
developers. 😄 The purpose of this chapter isn’t to convince you to do it one way and one way only.
The purpose is to show you just enough so you can understand these Combine examples in the
book and YOU choose how you and your team can implement them.
Many times I don’t even use a model but rather simple types just to save lines of code with the examples.
The Model may or may not have:
• Business logic or calculations
• Network access code
• Data validation
I’ve seen some projects use it as a very lightweight object with just the fields (like you see here). I have also
seen it as a very heavy object filled with all 3 of the points above. It’s up to you.
Sometimes the Model will be set up so it can easily be converted into JSON (Javascript Object Notation) and
back.
You will learn how to set this up later in the “Your Swift Foundation” chapter.
www.bigmountainstudio.com
13
Combine Mastery in SwiftUI
Architecture
View Model
The View Model is responsible for collecting your data and getting it ready to be presented on
the view. It will notify the view of data changes so the view knows to update itself.
class BookViewModel: ObservableObject {
@Published var books = [BookModel]()
func fetch() {
books =
[BookModel(name:
BookModel(name:
BookModel(name:
BookModel(name:
}
}
[
,
,
"SwiftUI
"SwiftUI
"Data in
"Combine
,
Views"),
Animations"),
SwiftUI"),
Reference")]
]
This is where you may or may not see things such as:
• Notifications to the view when data changes
• Updates to the data it exposes to the view (@Published property, in this example)
• Logic to validate data (may or may not be in the model)
• Functions to retrieve data (may or may not be in the model)
• Receive events from the view and act on it
You’re in Control
Architecture isn’t a one-size-fits-all solution.
You can consolidate or separate out of the view model as much as you want.
Remember, the goal of architecture is to make your life (and your team’s life) easier. So you
and your team decide how much you want to leave in or separate out to help achieve this goal.
Note: If you’re unfamiliar with ObservableObject or
@Published then you might want to read “Working with
Data in SwiftUI”.
@Published will also be covered later in this book.
For the purpose of demonstrating examples in this book, I will try to leave in all relevant
logic in the View Model to make it easier for you to read and learn and not have to skip
around or flip pages to connect all the dots.
14
􀉚
􀉚
􀉚
􀉚
www.bigmountainstudio.com
If separating out validation logic makes your life easier because it then becomes easier to test
or reuse in other places, then do it.
Combine Mastery in SwiftUI
Architecture
View
struct BookListView: View {
The View is the presentation of the data to the user.
@StateObject var vm = BookViewModel()
It is also where the user can interact with the app.
var body: some View {
List(vm.books) { book in
HStack {
Image(systemName: "book")
Text(book.name)
}
}
In SwiftUI, if you want to change what is showing on the
screen then you’ll have to change some data that drives
the UI.
Many of you, including myself, had to change the way we
thought about the View.
.onAppear {
vm.fetch()
}
Use width: 214
A Different Way of Thinking
You can’t reference UI elements and then access their
properties and update them directly in SwiftUI.
}
}
Instead, you have the UI updated based on the data it is
connected to.
For simplicity and condensing the examples
used in this book, I mostly use a view and an
observable object. Sometimes you will see
data objects.
In this example, I can’t say List.add(newBook) to add
a new row on the list.
Instead, I would update the data and the UI would
update automatically.
So I guess you could say this book uses
VOODO architecture: View - Observable
Object - Data Object. 😃
www.bigmountainstudio.com
15
Combine Mastery in SwiftUI
COMBINE CONCEPTS
You may wonder why the cover has a hand holding a pipe wrench (a tool used in plumbing). Well, you’re going to find out in this chapter.
This chapter is going to help you start thinking with Combine ideas and concepts so later you can turn those concepts into actual code.
Combine Concepts
Like Plumbing
Many of the terms you will find in the Apple documentation for Combine relate to water or plumbing.
The word “plumbing” means “systems of pipes, tanks, filtering and other parts required for getting water.”
You could say Combine is a system of code required for getting data.
I would like to sign up
for some water.
Water Source
(Water Tower)
www.bigmountainstudio.com
Pipeline
17
Water User
Combine Mastery in SwiftUI
Combine Concepts
Publishers & Subscribers
Combine consists of Publishers and Subscribers.
Publisher
Subscriber
A type that can push out data. It can push out the data all at once
or over time.
In English, “publish” means to “produce and send out to make
known”.
Something that can receive data from a publisher.
In English, “subscribe” means to “arrange to receive
something”.
I would like to sign up
for some data.
Sends data through the Pipeline
Publisher
www.bigmountainstudio.com
Subscriber
18
Combine Mastery in SwiftUI
Combine Concepts
Operators
Operators are functions you can put right on the pipeline between the Publisher and the Subscriber.
They take in data, do something, and then re-publish the new data. So operators ARE publishers.
They modify the Publisher much like you’d use modifiers on a SwiftUI view.
I would like to sign up for some clean
data but not too much all at once.
Publisher
www.bigmountainstudio.com
Filter
Operator
Pressure
Operator
19
Subscriber
Combine Mastery in SwiftUI
Combine Concepts
Upstream, Downstream
You will also see documentation (and even some types) that mention “upstream” and “downstream”.
Upstream
Downstream
“Upstream” means “in the direction of the PREVIOUS part”.
In Combine, the previous part is usually a Publisher or Operator.
“Downstream” means “in the direction of the NEXT part”.
In Combine, the next part could be another Publisher, Operator
or even the Subscriber at the end.
I have 2 operators and a
subscriber downstream from me.
Upstream
Publisher
www.bigmountainstudio.com
Downstream
Filter
Operator
Pressure
Operator
20
Subscriber
Combine Mastery in SwiftUI
Combine Concepts
Also, Like SwiftUI
Combine is also like SwiftUI?!?! What?
SwiftUI
Combine
In SwiftUI, you start with a View and you can add many modifiers to
that View.
With Combine, you start with a Publisher and you can add many operators
(modifiers) to that Publisher.
Each modifier returns a NEW, modified View:
Each operator returns a NEW, modified operator:
MyStringArrayPublisher
Text("Hello, World!")
.fakeOperatorToRemoveDuplicates()
.font(.largeTitle)
.fakeOperatorToRemoveNils()
.bold()
.fakeOperatorToFilterOutItems(thatBeginWith: “m”)
.underline()
.fakeOperatorToPublishTheseItemsEvery(seconds: 2)
.foregroundColor(.green)
.fakeSubscriberToAssignThisVariable(myResultVariable)
.padding()
(Note: These are fake names. 😃 )
But I think you get the idea. You start with a publisher
(MyStringArrayPublisher), you add operators to it that perform some
task on the published data, then the subscriber
(fakeSubscriberToAssignThisVariable) receives the result at the end
and does something with it.
www.bigmountainstudio.com
21
Combine Mastery in SwiftUI
YOUR SWIFT FOUNDATION
Before even diving into Combine, you need to build a solid Swift foundation. Once this foundation is in place, you’ll find it much easier to read Combine
documentation and code.
There are certain Swift language features that Combine heavily relies on. I will take you through most of these language features that make Combine possible to
understand.
If you find you are familiar with a topic presented here, then you can quickly flip through the pages but be sure to look at how the topic applies to Combine.
Two Types of Developers
There are two types of developers:
• Those who create code (application programming interfaces or APIs) to be used by other developers
• Those who consume APIs
Some developers are both types. But if you’re not used to creating APIs then you may not be too familiar with the following Swift language topics and therefore may
have a harder time understanding Combine, its documentation, and how it works.
Let’s walk through these topics together. I’m not saying you have to become an expert on these topics to use Combine, but having a general understanding of
these topics and how they relate to Combine will help.
Protocols
Protocols are a way to create a blueprint of properties and functions you want other classes and structs to contain.
This helps create consistency and predictability.
If you know that a specific protocol always has a “name” property, then it doesn’t matter what class or struct you are working with that uses this protocol, you know
that they will all ALWAYS have a “name” property.
You are not required to know anything else about the class or struct that follows this protocol. There might be a lot of other functions and properties. But because
you know about the protocol that class or struct uses then you also know they are ALL going to have that “name” property.
Protocols
Protocols Introduction
protocol PersonProtocol {
var firstName: String { get set }
var lastName: String { get set }
func getFullName() -> String
}
struct DeveloperStruct: PersonProtocol {
var firstName: String
var lastName: String
func getFullName() -> String {
return firstName + " " + lastName
}
By itself, a protocol does nothing and does not
contain any logic.
It simply defines properties and functions.
This struct “conforms to” or implements the
protocol. Meaning, it is required that all the
properties and functions are used within it.
}
Use width: 214
struct Protocol_Intro: View {
private var dev = DeveloperStruct(firstName: "Scott", lastName: "Ching")
var body: some View {
VStack(spacing: 20) {
HeaderView("Protocols",
subtitle: "Introduction",
desc: "Protocols allow you to define a blueprint of properties and
functions. Then, you can create new structs and classes that
conform or implement the protocol's properties and function.")
Text("Name: \(dev.getFullName())")
}
.font(.title)
}
}
www.bigmountainstudio.com
25
Combine Mastery in SwiftUI
Protocols
Protocols As a Type
class StudentClass: PersonProtocol {
var firstName: String
var lastName: String
init(first: String, last: String) {
firstName = first
lastName = last
}
}
This class also conforms to the
PersonProtocol on the previous page and
implements the getFullName function a little
differently.
func getFullName() -> String {
return lastName + ", " + firstName
}
struct Protocol_AsType: View {
var developer: PersonProtocol
var student: PersonProtocol
Notice the type for these properties is simply the
protocol. The properties can be assigned to any
value as long as that class or struct conforms to
this protocol.
var body: some View {
VStack(spacing: 20) {
HeaderView("Protocols",
subtitle: "As a Type",
desc: "You can set the type of a property using the Protocol. Any object
that conforms to this protocol type can be set to this property
now. It doesn't matter if it's a class or a struct!")
Use width: 214
Text(developer.getFullName())
Text(student.getFullName())
}
}
}
.font(.title)
One is a struct and the other is a class. It
doesn’t matter as long as they conform to
PersonProtocol.
struct Protocol_AsType_Previews: PreviewProvider {
static var previews: some View {
Protocol_AsType(
developer: DeveloperStruct(firstName: "Chris", lastName: "Smith"),
student: StudentClass(first: "Mark", last: "Moeykens"))
}
}
www.bigmountainstudio.com
26
Combine Mastery in SwiftUI
Protocols
How do Protocols relate to Combine?
Protocols allow Publishers (and Operators) to have the same functions and all Subscribers to have the same exact functions too.
protocol Publisher {
func receive(subscriber:)
}
protocol Publisher {
func receive(subscriber:)
}
Publishers (and operators) have a receive function that allows them to connect to subscribers.
www.bigmountainstudio.com
27
protocol
func
func
func
}
Subscriber {
receive(subscription:)
receive(input:)
receive(completion:)
The Subscriber protocol has 3 receive functions. Let’s
talk about these on the next page…
Combine Mastery in SwiftUI
Protocols
The 3 Subscriber Receive Functions
When comparing to getting water to your house, the 3 subscriber receive functions indicate when you successfully subscribe to water, when you receive water and
when you end your water service to your house.
1
func receive(subscription:)
OK, we got your subscription
and you can now get water.
www.bigmountainstudio.com
2
func receive(input:)
Yay! I’m getting water!
28
3
func receive(completion:)
Your water service is now
complete and we turned it off.
Combine Mastery in SwiftUI
Protocols
Publisher & Subscriber Protocols
protocol
func
func
func
}
protocol Publisher {
func receive(subscriber:)
}
The goal here is to give you an understanding of how protocols work
and introduce you to the two major protocols behind Combine. Yes,
these two protocols are implemented by all of the publishers, operators,
and subscribers you will be working with.
?
You may have noticed I’m not showing you
the types for these functions yet.
They are set up in a way to allow the
developer to provide different types.
You WILL NOT have to conform to these protocols yourself. The
Combine team did all of this for you! These protocols make sure you
can connect all publishers, operators, and subscribers together like
pipes in a plumbing system.
This is allowed through the use of
“generics” in the Swift language.
Let’s learn more about how that works in
the next section…
Most likely you will never have to create a class that conforms to these
protocols in your career with Combine.
www.bigmountainstudio.com
Subscriber {
receive(subscription:)
receive(input:)
receive(completion:)
29
Combine Mastery in SwiftUI
Generics
<T>
Swift is a strongly typed language, meaning you HAVE to specify a type (like Bool, String, Int, etc.) for variables and parameters.
But what if your function could be run with any type? You could write as many functions as there are types.
OR you could use generics and write ONE function so the developer using the function specifies the type they want to use.
It’s pretty cool, so let’s take a look at how this is done.
Generics
Generics Introduction
struct Generics_Intro: View {
@State private var useInt = false
@State private var ageText = ""
//
//
//
//
//
//
func getAgeText<T>(value1: T) -> String {
return String("Age is \(value1)")
}
func getAgeText(value1: Int) -> String {
return String("Age is \(value1)")
}
func getAgeText(value1: String) -> String {
return String("Age is \(value1)")
}
The <T> is called a “type placeholder”. This
indicates a generic is being used and you
can substitute T with any type you want.
That one generic function can now replace
these two functions.
var body: some View {
VStack(spacing: 20) {
HeaderView("Generics",
subtitle: "Introduction",
desc: "A generic variable allows you to create a type placeholder that
can be set to any type the developer wants to use.")
Group {
Toggle("Use Int", isOn: $useInt)
Button("Show Age") {
if useInt {
Because the parameter
ageText = getAgeText(value1: 28)
} else {
type is generic, you can
ageText = getAgeText(value1: "28")
pass in any type.
}
}
Text(ageText)
}
.padding(.horizontal)
}
.font(.title)
}
Use width: 214
}
www.bigmountainstudio.com
31
Combine Mastery in SwiftUI
Generics
Generics On Objects
struct Generic_Objects: View {
The generic (<T>) is declared on the class
so now the scope extends to all members
within this class.
class MyGenericClass<T> {
var myProperty: T
init(myProperty: T) {
self.myProperty = myProperty
}
}
var body: some View {
let myGenericWithString = MyGenericClass(myProperty: "Mark")
let myGenericWithBool = MyGenericClass(myProperty: true)
Use width: 214
You can initialize
the class with
different types.
VStack(spacing: 20) {
HeaderView("Generics",
subtitle: "On Objects",
desc: "Generics can also be applied to classes and structs to make a type
available to all properties and functions within them.")
Text(myGenericWithString.myProperty)
Text(myGenericWithBool.myProperty.description)
}
So you see, the <T> doesn’t mean the
class IS a generic. It means the class
CONTAINS a generic within it that can be
shared among all members (properties
and functions).
.font(.title)
}
}
www.bigmountainstudio.com
32
Combine Mastery in SwiftUI
Generics
Multiple Generics
struct Generic_Multiple: View {
class MyGenericClass<T, U> {
var property1: T
var property2: U
Keep adding additional letters or names
separated by commas for your generic
placeholders like this.
init(property1: T, property2: U) {
self.property1 = property1
self.property2 = property2
}
}
var body: some View {
let myGenericWithString = MyGenericClass(property1: "Joe", property2: "Smith")
let myGenericWithIntAndBool = MyGenericClass(property1: 100, property2: true)
Use width: 214
VStack(spacing: 20) {
HeaderView("Generics",
subtitle: "Multiple",
desc: "You can declare more than one generic.")
Text("\(myGenericWithString.property1) \(myGenericWithString.property2)")
Text("\(myGenericWithIntAndBool.property1) \
(myGenericWithIntAndBool.property2.description)")
DescView("The convention is to start with 'T' and continue down the alphabet when
using multiple generics. \n\nBut you will notice in Combine more
descriptive names are used.")
}
.font(.title)
}
}
www.bigmountainstudio.com
33
Combine Mastery in SwiftUI
Generics
Generics - Constraints
struct Generics_Constraints: View {
private var age1 = 25
private var age2 = 45
func getOldest<T: SignedInteger>(age1: T, age2: T) -> String {
if age1 > age2 {
return "The first is older."
} else if age1 == age2 {
return "The ages are equal"
}
return "The second is older."
}
You can specify your
constraint the same way you
specify a parameter’s type.
SignedInteger is a protocol
adopted by Int, Int8, Int16,
Int32, and Int64. So T can
be any of those types.
var body: some View {
VStack(spacing: 20) {
HeaderView("Generics",
subtitle: "Constraints",
desc: "Maybe you don't want a generic to be entirely generic. You can
narrow down just how generic you want it to be with a
‘constraint'.")
Use width: 214
Don’t worry, Xcode will tell you if
the constraint you want to use will
work or not.
HStack(spacing: 40) {
Text("Age One: \(age1)")
Text("Age Two: \(age2)")
}
Text(getOldest(age1: age1, age2: age2))
DescView("Note: Constraints are usually protocols.")
}
.font(.title)
Note: Constraints can be used where ever you can add a
generic declaration, not just on functions like you see here.
}
}
www.bigmountainstudio.com
34
Combine Mastery in SwiftUI
Generics
How do Generics relate to Combine?
Generics allow the functions of many Publishers, Operators, and Subscribers to work with the data types you provide or start with. The data types you are publishing
to your UI might be an Int, String, or a struct.
func PublishData<Output, Failure>(...)
func FilterData<Output, Failure>(...)
func SubscriberToData<Input, Failure>(...)
(Note: These are not real function names. For demonstration only.)
Whatever type you start with, it will continue all the way down the pipeline unless you intentionally change it.
These functions can also have errors or failures. The failure’s type can be different for different publishers, operators, and subscribers.
www.bigmountainstudio.com
35
Combine Mastery in SwiftUI
Associatedtype &
Typealias
?
You can’t declare protocols with generics like you can with structs and classes. If you try, you will get an error: “Protocols do not allow generic parameters.”
So what do you do?
You use the associatedtype keyword. This is something the Publisher and Subscriber protocols make use of.
associatedtype & typealias
AssociatedType & Typealias Introduction
protocol GameScore {
associatedtype TeamScore // This can be anything: String, Int, Array, etc.
func calculateWinner(teamOne: TeamScore, teamTwo: TeamScore) -> String
}
struct FootballGame: GameScore {
typealias TeamScore = Int
You use associatedtype to indicate it can
be any type.
You use typealias to declare the type when
conforming to the protocol.
func calculateWinner(teamOne: TeamScore, teamTwo: TeamScore) -> String {
if teamOne > teamTwo {
return "Team one wins"
Use width: 214
} else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
The calculateWinner function will use whatever
type TeamScore is to try and calculate which one
wins.
}
struct AssociatedType_Intro: View {
var game = FootballGame()
private var team1 = Int.random(in: 1..<50)
private var team2 = Int.random(in: 1..<50)
@State private var winner = ""
www.bigmountainstudio.com
37
Combine Mastery in SwiftUI
associatedtype & typealias
var body: some View {
VStack(spacing: 20) {
HeaderView("AssociatedType",
subtitle: "Introduction",
desc: "When looking at Apple's documentation you see 'associatedtype'
used a lot. It's a placeholder for a type that YOU assign when you
adopt the protocol.")
HStack(spacing: 40) {
Text("Team One: \(team1)")
Text("Team Two: \(team2)")
}
Use width: 214
Button("Calculate Winner") {
winner = game.calculateWinner(teamOne: team1, teamTwo: team2)
}
Text(winner)
Spacer()
}
.font(.title)
}
}
www.bigmountainstudio.com
38
Combine Mastery in SwiftUI
associatedtype & typealias
Instead of using typealias…
struct FootballGame: GameScore {
//
typealias TeamScore = Int // Not needed if explicitly set below:
Although you can use typealias to set types for associated
types in protocols, you don’t always have to use them.
func calculateWinner(teamOne: Int, teamTwo: Int) -> String {
if teamOne > teamTwo {
You could explicitly set the type where it is used, like in the
calculateWinner function signature here.
return "Team one wins"
} else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
}
www.bigmountainstudio.com
39
Combine Mastery in SwiftUI
associatedtype & typealias
Potential Problem
struct SoccerGame: GameScore {
typealias TeamScore = String
TeamScore can be set to any type. And you may get
unexpected results depending on the type you use.
func calculateWinner(teamOne: TeamScore, teamTwo: TeamScore) -> String {
Like generics, you can also set type constraints so the
developer only uses a certain category of types that, for
example, match a certain protocol.
if teamOne > teamTwo {
return "Team one wins"
}else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
}
www.bigmountainstudio.com
40
Combine Mastery in SwiftUI
associatedtype & typealias
Constraints
protocol Teams {
// This can be any type of collection, such as: Dictionary, Range, Set
associatedtype Team: Collection
var team1: Team { get set }
var team2: Team { get set }
The way you define a type constraint is the
same format you use for variables or even
generic constraints by using the colon followed
by the type.
func compareTeamSizes() -> String
}
struct WeekendGame: Teams {
var team1 = ["Player One", "Player Two"]
Use width: 214
var team2 = ["Player One", "Player Two", "Player Three"]
func compareTeamSizes() -> String {
if team1.count > team2.count {
return "Team 1 has more players"
} else if team1.count == team2.count {
return "Both teams are the same size"
}
Notice in this example I’m not using a type alias
to define what type Team is.
Instead, I’m explicitly using a string array
which Swift will understand and set the type for
me.
return "Team 2 has more players"
}
}
www.bigmountainstudio.com
41
Combine Mastery in SwiftUI
associatedtype & typealias
Constraints - View
struct AssociatedType_Constraints: View {
@State private var comparison = ""
private let weekendGame = WeekendGame()
var body: some View {
VStack(spacing: 20) {
HeaderView("Constraints",
subtitle: "On Associated Types",
desc: "You can limit the generic type for the associated type the same
way you do with generics.")
Button("Evaluate Teams") {
Use width: 214
comparison = weekendGame.compareTeamSizes()
}
Text(comparison)
Spacer()
}
.font(.title)
}
}
www.bigmountainstudio.com
42
Combine Mastery in SwiftUI
associatedtype & typealias
How do associated types relate to Combine?
As you know, Combine has a protocol for the Publisher and the Subscriber. Both protocols define inputs, outputs, and failures using associated types.
String or struct, etc.
protocol Publisher {
protocol Subscriber {
associatedtype Output
associatedtype Input
associatedtype Failure: Error
associatedtype Failure: Error
}
}
Publishers can publish any type you want for its output. Output could
be simple types like String, Int, or Bools or structs of data you get
from another data source.
The Failure generic is constrained to the Error protocol.
www.bigmountainstudio.com
Subscribers can receive any input from the connected publisher.
The Failure generic is constrained to the Error protocol.
Note: The Error protocol doesn’t have any members. It just allows
your struct or class to be used where the Error type is expected.
43
Combine Mastery in SwiftUI
associatedtype & typealias
Matching Publishers with Subscribers
When putting together a pipeline of publishers, operators, and subscribers, all the output types and the subscriber’s input types have to be the same.
Publisher Output
Subscriber Input
struct
Int
The pipes (types) have to match!
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
}
public protocol Subscriber {
associatedtype Input
associatedtype Failure : Error
}
The Output must match the Input type for this pipeline to work. The Failure types also have to match.
How could you enforce these rules within a protocol though? You use a generic “where clause”. Keep reading…
www.bigmountainstudio.com
44
Combine Mastery in SwiftUI
Generic Where Clauses
You know about generic constraints from the previous sections. The generic where clause is another way to set or limit conditions in which you can use a protocol.
You can say things like, “If you use this protocol, then this generic type must match this other generic type over here.” Combine does this between publishers and
subscribers.
By the way, the word “clause” just means “a required condition or requirement” here.
Generic Where Clauses
Generic Where Clause - Introduction
Here are two protocols that work together.
We leave it to the developer to choose which type to use for SkillId. Maybe skills are represented with a String or maybe an Int.
Whatever type is selected though, the types between the Job and Person have to match so a Person can be assigned jobs.
protocol Job {
protocol Person {
associatedtype SkillId
associatedtype SkillId
var id: SkillId { get set }
var knows: SkillId { get set }
}
func assign<J>(job: J) where J : Job, Self.SkillId == J.SkillId
}
We want to enforce
that these types match
when assigning a job.
This where clause is telling us that the SkillId type from both
protocols must be the same for this function to work.
Note: Earlier I mentioned a common abbreviation for declaring
a generic type is “T”. In this example I’m using “J” to represent
“Job”. You will commonly see this pattern in Combine
documentation.
www.bigmountainstudio.com
46
Combine Mastery in SwiftUI
Generic Where Clauses
How do Generic Where Clauses relate to Combine?
We know that Publishers send out values of a particular type. Subscribers only work if they receive the exact same type. For example, if the publisher publishes an
array, the subscriber has to receive an array type.
String or struct, etc.
public protocol Publisher {
public protocol Subscriber {
associatedtype Output
associatedtype Input
associatedtype Failure : Error
associatedtype Failure : Error
}
func receive<S>(subscriber: S) where S : Subscriber,
Self.Failure == S.Failure,
Self.Output == S.Input
The Publisher uses a generic where clause here to make sure the
Failure types match up and the Publisher’s output type matches
the Subscriber’s input type.
This is how Combine makes sure the pipes between
Publisher and Subscriber always fit together.
}
www.bigmountainstudio.com
47
Combine Mastery in SwiftUI
@PUBLISHED
The @Published property wrapper is one of the easiest ways to get started with Combine. It automatically handles the publishing of data for you when it’s used in a
class that conforms to the ObservableObject protocol.
@Published
Concepts
You use the @Published property wrapper inside a class that conforms to
ObservableObject.
When the @Published properties change they will notify any view that
subscribes to it.
The view can subscribe to this ObservableObject by using the
@StateObject property wrapper, for example.
Publisher (View Model)
@Published data
www.bigmountainstudio.com
Subscriber (View)
Notify view of any changes
49
View
Combine Mastery in SwiftUI
@Published
Template
The code between the ObservableObject and View might look something like this:
Publisher (View Model)
Subscriber (View)
class MyViewModel: ObservableObject {
@Published var data = “Some Data”
struct Published_Intro: View {
@StateObject var vm = MyViewModel()
Notify view of any changes
}
}
SwiftUI property wrappers make it really easy to subscribe to publishers.
• ObservableObject - Lets the View know that one of the @Published property values has changed.
• @Published - This is the publisher. It will send out or publish the new values when changed.
• @StateObject - This is the subscriber. It’ll receive notifications of changes. It will then find where @Published properties are being used within the view, and then
redraw that related view to show the updated value.
Let’s look at more examples…
www.bigmountainstudio.com
50
Combine Mastery in SwiftUI
􀎷
@Published
Introduction
class PublishedViewModel: ObservableObject {
@Published var state = "1. Begin State"
init() {
// Change the name value after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = "2. Second State"
}
}
}
After 1 second, the state
property is updated.
When an update happens,
the observable object
publishes a notification so
that subscribers can update
their views.
struct Published_Intro: View {
@StateObject private var vm = PublishedViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("@Published",
subtitle: "Introduction",
desc: "The @Published property wrapper with the ObservableObject is the
publisher. It sends out a message to the view whenever its value
has changed. The StateObject property wrapper helps to make the
view the subscriber.")
Text(vm.state)
DescView("When the state property changes after 1 second, the UI updates in
response. This is read-only from your view model.")
}
.font(.title)
}
}
www.bigmountainstudio.com
Nowhere in this example am I manually telling the View to update nor change the
text view. It all happens automatically. This is the power of SwiftUI & Combine.
51
Combine Mastery in SwiftUI
@Published
Sequence
Publisher (View Model)
Subscriber (View)
Subscription (Connection) Established
Send the current value: “1. Begin State”
Send the updated value: “2. Second State”
www.bigmountainstudio.com
52
Combine Mastery in SwiftUI
􀎷
@Published
Read and Write
class PublishedViewModel: ObservableObject {
@Published var state = "1. Begin State"
init() {
// Change the name value after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = "2. Second State"
}
}
}
struct Published_ReadWrite: View {
@StateObject private var vm = PublishedViewModel()
The @Published property
will get updated directly
when using two-way binding.
var body: some View {
VStack(spacing: 20) {
HeaderView("@Published",
subtitle: "Read and Write",
desc: "Using a dollar sign ($) we can create a two-way binding.")
TextField("state", text: $vm.state)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text(vm.state)
DescView("You can now send this value back to the view model automatically.")
}
.font(.title)
}
}
www.bigmountainstudio.com
53
Combine Mastery in SwiftUI
􀎷
@Published
Validation with onChange
class PublishedValidationViewModel: ObservableObject {
@Published var name = ""
}
struct Published_Validation: View {
@StateObject private var vm = PublishedValidationViewModel()
@State private var message = ""
var body: some View {
VStack(spacing: 20) {
HeaderView("@Published",
subtitle: "onChange",
desc: "You could use the onChange to validate data entry. While this
works, you may want to move this logic to your view model.")
HStack {
TextField("name", text: $vm.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: vm.name, perform: { value in
message = value.isEmpty ? "❌ " : "✅ "
})
Text(message)
}
If we were to move this validation logic into the view
model, how would we do it?
We can use Combine to handle this for us. Let’s
create your first Combine pipeline!
.padding()
}
.font(.title)
}
}
www.bigmountainstudio.com
54
Combine Mastery in SwiftUI
YOUR FIRST PIPELINE
Data
✅
or
❌
I’m going to walk you through your first Combine pipeline.
“But wait, Mark, wasn’t using @Published my first pipeline?”
It was, but that pipeline was created and connected by property wrappers so SwiftUI did it for us. It’s time to level up your Combine skills!
Your First Pipeline
The Plan
class YourFirstPipelineViewModel: ObservableObject {
@Published var name: String = ""
@Published var validation: String = ""
init() {
// Create pipeline here
The validation result will be
assigned to this property.
You’re going to create your
new pipeline here!
}
}
struct YourFirstPipeline: View {
@StateObject private var vm = YourFirstPipelineViewModel()
var body: some View {
Use width: 214
VStack(spacing: 20) {
HeaderView("First Pipeline",
subtitle: "Introduction",
desc: "This is a simple pipeline you can create in Combine to validate a
text field.")
HStack {
TextField("name", text: $vm.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(vm.validation)
}
.padding()
}
The layout is the same as the example in the previous chapter.
.font(.title)
}
Now let’s look at the pieces you will need for your pipeline.
}
www.bigmountainstudio.com
56
Combine Mastery in SwiftUI
Your First Pipeline
The Pieces
Your pipeline always starts with a publisher
and always ends with a subscriber.
Publisher
The publisher sends out data. But data is
only sent out if someone wants it.
Just like a water tower, if no one is
subscribing to water service then that
water will just sit there and not flow
through the pipeline.
www.bigmountainstudio.com
Operator
The operator is where you put logic to do
something to the data flowing through the
pipeline.
This is where you can evaluate, modify
and somehow affect the data and its flow.
57
Subscriber
The subscriber is what requests the
data.
A house that subscribes to water requests
it to wash dishes, provide baths, etc.
You’ll be happy to know there are only a
few subscribers in Combine.
Combine Mastery in SwiftUI
Your First Pipeline
The Publisher
Your pipeline always starts with a publisher.
So where do you get one?
By using the dollar sign ($) in front of the @Published property name, you have direct access to its publisher!
class YourFirstPipelineViewModel: ObservableObject {
@Published var name: String = ""
@Published var validation: String = ""
init() {
// Create pipeline here
$name
The name property is a String
type.
But $name is of type Publisher.
So what is this Publisher?
}
}
Apple says it is: “A publisher for
properties marked with the
@Published attribute.”
(Hold down OPTION and click on $name to get this quick help to pop up.)
In this case, it will send down a
String.
Published<String> is the type of this @Published property
here. This means a String is sent down the pipeline.
www.bigmountainstudio.com
To you, that means it can be the
start of a Combine pipeline and
can send values down that
pipeline.
58
Combine Mastery in SwiftUI
Your First Pipeline
The Operator
You now need an operator that can evaluate every value that comes down through your pipeline to see if it’s empty or not.
You can use the map operator to write some code using the value coming through the pipeline.
✅
or
❌
class YourFirstPipelineViewModel: ObservableObject {
@Published var name: String = ""
@Published var validation: String = ""
The map operator allows you to run some code for every value that
comes through this pipeline.
init() {
// Create pipeline here
$name
Right now, only one string is coming through at a time.
.map({ (name) in
if name.isEmpty {
But later in this book you will see MANY examples of how multiple
values can be published.
return "❌ "
} else {
In a few pages, I’m going to show you some alternative ways in which
you can write this map operator logic you see here. What I’m showing
you here is the “long way” but it can be easier to follow.
return "✅ "
}
})
}
}
OK, we have a publisher and an operator. We still need the third piece, the subscriber.
www.bigmountainstudio.com
59
Combine Mastery in SwiftUI
Your First Pipeline
Why is it called “map”?
The term “map” is believed to originally date back to map makers who processed a set of data (longitude and latitude) to plot or draw a map.
Place
Longitude
Latitude
Berlin
52° N
13° E
Delhi
28° N
77° E
London
51° N
0° W
Mexico City
19° N
99° W
Moscow
55° N
37° E
Paris
48° N
2° E
Salt Lake City
40° N
111° W
São Paulo
23° S
46° W
Tokyo
35° N
139° E
It has since been adopted by
mathematics and then by the
computer science field to mean the
processing of a set of data in some
way.
In Combine, the map operator gives
you an easy way to run some code
on all data that comes down
through the pipeline; such as doing
validation.
Photo: Ylanite Koppens
www.bigmountainstudio.com
60
Combine Mastery in SwiftUI
Your First Pipeline
Rewriting the Map Logic
There are a few alternative ways we can rewrite this logic that you might be interested in. No specific way is more correct than another. It’s up to the standards you
set for yourself or the standards your development team agrees on.
Take a look at some of these options:
Original
.map({ (name) in
if name.isEmpty {
return "❌ "
Shorter
Shortest
.map { name in
return name.isEmpty ? "❌ " : "✅ "
}
.map { $0.isEmpty ? "❌ " : “✅ " }
} else {
return "✅ "
}
• You can remove the first set of
})
•
•
www.bigmountainstudio.com
parentheses. If the last (or only)
parameter is a closure, then we don’t
need the parentheses. This is called a
“trailing closure”.
You don’t need the parentheses around
the value that is being passed into the
closure either. Xcode will add it
automatically but you can remove it.
Instead of using if then, you can use a
Ternary operator ( Condition ? True
part : False part ).
61
• In a recent version of Swift, the return
•
•
keyword was made optional if you only
had one line of code in your function/
closure. This is called an “implicit return”.
The $0 notation can be used in place of
the first parameter that is passed into the
closure. These are called “anonymous
closure arguments” or “shorthand
argument names”. More info here.
The braces don’t have to be on separate
lines. This is a choice the developer can
make.
Combine Mastery in SwiftUI
Your First Pipeline
The Subscriber
The subscriber is required or else the publisher has no reason to publish data. The subscriber you’re going to use makes it super easy to get the value at the end of
the pipeline and assign it to your other published property called validation.
Assign Subscriber
class YourFirstPipelineViewModel: ObservableObject {
@Published var name: String = ""
@Published var validation: String = ""
init() {
// Create pipeline here
$name
.map { $0.isEmpty ? "❌ " : "✅ " }
.assign(to: &$validation)
}
The assign(to: ) subscriber
will take the data coming down
the pipeline and just drop it
right into the @Published
property you have specified.
Yeah, it really is that easy. 😃
value
}
@Published
Property
Note: The assign(to: ) ONLY works
with @Published properties.
What is the ampersand (&) and dollar sign ($) for?
(See next page…)
www.bigmountainstudio.com
62
Combine Mastery in SwiftUI
Your First Pipeline
Ampersand and Dollar Sign
What is the ampersand and dollar sign for?
Ampersand (&)
Dollar Sign ($)
When you pass a parameter into a function, you cannot alter its
value. It is considered a constant.
The @Published property wrapper turns the property into a
publisher, meaning it can now notify anyone listening of changes, like
your view.
To access the value of the property, you just use the name of the
property like this:
To make the parameter editable, add the inout keyword which
means the parameter can be updated after the function has run:
func doubleThis(value: inout Int) {
value = value * 2
}
var y = 4
doubleThis(value: &y)
let vm = YourFirstPipelineViewModel()
let name: String = vm.name
The ampersand is an indication
that says:
But if you want access to the Publisher itself, you will have to use the
dollar sign like this:
let namePublisher = vm.$name
Hey, this function can
and probably will change
the value that you are
passing in here.
It might make more sense if I include the type:
let namePublisher: Published<String>.Publisher = vm.$name
www.bigmountainstudio.com
63
Combine Mastery in SwiftUI
Your First Pipeline
@Published and Publisher
As you can see, the @Published property wrapper gives your property two parts:
1. The Property
2. The Publisher
The property part is just like a regular property.
The Publisher portion, accessible through the dollar sign, allows you to attach a pipeline to it.
Reading and writing to it is just as you would expect:
Think of it as an open pipe in which you can now attach other pipes (operators and subscribers).
class ViewModel: ObservableObject {
@Published var message = "Hello, World!"
}
let vm = ViewModel()
print(vm.message)
vm.message = "Hello, Developer!"
print(vm.message)
The Property
message
$message
(Playgrounds output)
www.bigmountainstudio.com
The Publisher
64
Combine Mastery in SwiftUI
Your First Pipeline
Does the Pipeline run Before or After the property is set?
The SwiftUI TextField has a binding directly to the @Published name property.
When a user types in a value, does the property get set first, and then the pipeline is run?
Add a couple of print statements so when you run the app, you
can see the output in the debugging console window.
class YourFirstPipelineViewModel: ObservableObject {
@Published var name: String = ""
@Published var validation: String = ""
init() {
$name
.map {
print("name property is now: \(self.name)")
print("Value received is: \($0)")
return $0.isEmpty ? "❌ " : "✅ "
}
.assign(to: &$validation)
}
}
As you can see for @Published properties bound to the UI, the pipeline is run
FIRST, before the property is even set.
www.bigmountainstudio.com
65
Combine Mastery in SwiftUI
Your First Pipeline
Assign(to: ) - Operator or Subscriber?
I’m calling the assign(to: ) function a “Subscriber”. And Apple categorizes this function as a Subscriber as well.
For simplicity, let’s stick with
calling it a “Subscriber”.
I believe they use “operator” instead
because this subscriber is missing one
essential ability that all subscribers can
do: cancel a publisher (turn off the
water) after it has started.
(You can learn more about this coming
up next.)
But you might notice further in the documentation that Apple also calls this function an “operator”.
This subscriber does not allow you to
cancel because it actually does it for
you! Very handy.
Let me get that for you.
www.bigmountainstudio.com
66
Combine Mastery in SwiftUI
􀎷
Your First Pipeline
Warning ⚠ - Avoid Recursion
The word “recursion” means to do something over and over again as a result of a function calling itself. You can easily make this happen by assigning the result of a
pipeline to the same publisher that started it. Here’s an example:
Play this video clip and watch what happens:
class ViewModel: ObservableObject {
@Published var message = "Hello, World!"
init() {
$message
.map { message in
message + " And you too!"
}
.assign(to: &$message)
}
🚩
Don’t do this! 😃
}
let vm = ViewModel()
print(vm.message)
What’s happening?
The pipeline gets triggered as soon as a value is set to the message
property.
So the end of the pipeline is setting a new value to message which then
triggers the pipeline when sets a new value to message which triggers the
pipeline… you get the idea.
www.bigmountainstudio.com
67
Combine Mastery in SwiftUI
Your First Pipeline
Summary
Congratulations on
building your first
Combine pipeline!
✅
or
❌
Let’s summarize
some of the things
you have learned.
Publisher
Operator
You learned you could
use @Published
properties as Publishers
to create your pipelines.
You learned about your
first operator: map.
You access the Publisher
part of the @Published
property by using the
dollar sign ($).
The map function
accepts values coming
down the pipeline and
can evaluate and run
logic on them.
When it’s done, it sends
the new value
downstream through the
pipeline.
www.bigmountainstudio.com
68
Subscriber
You learned about the
assign subscriber
which will take data
coming down the
pipeline and assign it to
a property.
@Published
Property
This particular function
can ONLY work with
@Published properties.
Combine Mastery in SwiftUI
YOUR FIRST
CANCELLABLE PIPELINE
ON
OFF
The assign(to: ) subscriber you used in the previous chapter was always open. Meaning, it always allowed data to stream through the pipeline. Once created, you
couldn’t turn it off.
There is another subscriber you can use that gives you the ability to turn off the pipeline’s data stream at a later time. I call this a “Cancellable Subscriber”.
Your First Cancellable Pipeline
The Sink Subscriber
The cancellable subscriber I’m talking about is called “sink”.
“Wait, Mark, you’re joking right?”
Ha ha, I’m completely serious! The sink subscriber is where your water… I mean, “data”, flows into. You can do what you want once you have data in the sink. You can
validate it, change it, make decisions with it, assign it to other properties, etc.
Da
ta
ta
Data
Da
You can do anything you
want once the data is in
your sink.
Data
The sink subscriber has a convenient way
of stopping the flow of data.
We call it a “handle”. Apple calls it a
“cancel” function.
Data
Let’s see what this looks like in code…
www.bigmountainstudio.com
70
Combine Mastery in SwiftUI
Your First Cancellable Pipeline
Before & After
Let’s convert the first view model to use the sink subscriber instead of the assign subscriber.
Before
After
class YourFirstPipelineViewModel: ObservableObject {
import Combine
@Published var name: String = ""
@Published var validation: String = ""
class FirstPipelineUsingSinkViewModel: ObservableObject {
@Published var name: String = ""
init() {
@Published var validation: String = ""
// Create pipeline here
Import Combine
$name
.map { $0.isEmpty ? "❌ " : "✅ " }
.assign(to: &$validation)
}
var cancellable: AnyCancellable?
init() {
From this point on, you will
need to import Combine for
all of your view models.
cancellable = $name
.map { $0.isEmpty ? "❌ " : "✅ " }
}
.sink { [unowned self] value in
self.validation = value
}
}
}
The sink subscriber returns an AnyCancellable class.
This class conforms to the Cancellable protocol which has just
one function, cancel().
www.bigmountainstudio.com
71
public protocol Cancellable {
func cancel()
}
Combine Mastery in SwiftUI
Your First Cancellable Pipeline
What if I don’t store the AnyCancellable returned from sink?
If you do not store a reference to the AnyCancellable returned from sink then Xcode will give you a warning.
The warning should also tell you that your pipeline will immediately be cancelled after init completes!
Run once?
If you only want to run the pipeline one time and not show the warning
then use the underscore like this:
init() {
_ = $name
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.validation = value
}
(The underscore just means you are not using the result of the function.)
But be warned, if you have an operator that delays execution, the pipeline
}
may never finish because it is deinitialized after the init() completes.
www.bigmountainstudio.com
72
Combine Mastery in SwiftUI
􀎷
Your First Cancellable Pipeline
The View
struct FirstPipelineUsingSink: View {
@StateObject private var vm = FirstPipelineUsingSinkViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("First Pipeline",
subtitle: "With Sink",
desc: "The validation is now being assigned using the sink subscriber.
This allows you to cancel the subscription any time you would
like.")
HStack {
TextField("name", text: $vm.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(vm.validation)
}
.padding()
Button("Cancel Subscription") {
vm.validation = ""
vm.cancellable?.cancel()
}
}
.font(.title)
}
}
www.bigmountainstudio.com
When you play this video, notice that
after cancelling the subscription, the
validation no longer happens.
73
On the previous page, the cancellable
property was public. We can access it directly
to call the cancel function to cancel the
validation subscription.
You may want to keep your cancellable
private and instead expose a public
function you can call. See next page for an
example of this…
Combine Mastery in SwiftUI
􀎷
Your First Cancellable Pipeline
Long-Running Process - View Model
class LongRunningProcessViewModel: ObservableObject {
@Published var data = "Start Data"
@Published var status = ""
private var cancellablePipeline: AnyCancellable?
In this view model, the cancellable
property is private.
init() {
cancellablePipeline = $data
.map { [unowned self] value -> String in
status = "Processing..."
return value
}
.delay(for: 5, scheduler: RunLoop.main)
.sink { [unowned self] value in
status = "Finished Process"
}
}
Note: I’m using the delay operator to
simulate a process that might take a long
time.
I specified a 5-second delay (the for
parameter).
The scheduler is basically a mechanism
to specify where and how work is done. I’m
specifying I want work done on the main
thread.
func refreshData() {
data = "Refreshed Data"
}
func cancel() {
status = "Cancelled"
cancellablePipeline?.cancel()
// OR
The cancelling functionality is now in a public cancel
function that the view can call.
cancellablePipeline = nil
}
}
www.bigmountainstudio.com
74
Combine Mastery in SwiftUI
Your First Cancellable Pipeline
Long-Running Process - View
struct LongRunningProcess: View {
@StateObject private var vm = LongRunningProcessViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Cancellable Pipeline",
subtitle: "Long-Running Process",
desc: "In this example we pretend we have a long-running process that we
can cancel before it finishes.")
Text(vm.data)
Button("Refresh Data") {
Use width: 214
vm.refreshData()
}
Button("Cancel Subscription") {
vm.cancel()
}
Call the cancel function
here to stop the pipeline.
.opacity(vm.status == "Processing..." ? 1 : 0)
Text(vm.status)
}
.font(.title)
}
}
www.bigmountainstudio.com
75
Combine Mastery in SwiftUI
Your First Cancellable Pipeline
Unowned Self
In many of these code examples you see me using [unowned self]. Why?
Closures
ViewModel
Pipeline
cancellable
ViewModel.status
When you see code like this between opening
and closing braces ( {…} ) it’s called a “closure”.
.sink { [unowned self] value in
self.status = “This is in a closure”
}
A closure is taking that code and sending it to
another object to be run.
But notice the closure contains a reference
self.status. This means that the pipeline
now has a reference to the view model.
And now, you are keeping a reference of the
pipeline through the cancellable property.
The pipeline has a reference to the
ViewModel.
Make this reference weak or unowned
to prevent a circular reference.
This is a circular reference.
One of these objects cannot deinititialize (be
removed from memory) until the other one is
removed first…UNLESS you make one of the
references weak or unowned.
www.bigmountainstudio.com
76
Combine Mastery in SwiftUI
Your First Cancellable Pipeline
Pipeline Lifecycle
Is [unowned self] better than [weak self]?
Unowned
class LongRunningProcessViewModel: ObservableObject {
2
Cancellables are removed from memory.
All pipelines are cancelled.
Data can no longer be sent down the
pipelines.
@Published var status = ""
private var cancellablePipeline: AnyCancellable?
init() {
cancellablePipeline = $data
This means the sink’s closure will no longer
run.
.map { [unowned self] value -> String in
status = "Processing..."
This is true for the scenario we have here
where the sink is referencing something
within the same class (view model).
3
return value
Anything that was running within the
closures stopped when the cancellables
were cancelled and destroyed.
}
.delay(for: 5, scheduler: RunLoop.main)
.sink { [unowned self] value in
Weak
www.bigmountainstudio.com
Class is removed from memory.
@Published var data = "Start Data"
In this case, you can use [unowned
self]because when the ViewModel class is
de-initialized, the cancellablePipeline property
will also cancel and de-initialize which will
destroy the related subscriber.
If you have a scenario where the sink is
referencing something OUTSIDE the view
model class and you can’t guarantee that
outside reference will de-initialize first, then
you better use [weak self]instead.
1
status = "Finished Process"
3
}
}
. . .
}
77
Combine Mastery in SwiftUI
CANCELLING MULTIPLE
PIPELINES
So far, you have seen how to store and cancel one pipeline. In some cases, you will have multiple pipelines and you might want to cancel all of them all at one time.
Cancelling Multiple Pipelines
Store(in:) - View
struct CancellingMultiplePipelines: View {
@StateObject private var vm = CancellingMultiplePipelinesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Store",
subtitle: "Introduction",
desc: "You can use the store function at the end of a pipeline to add
your pipeline's cancellable to a Set.")
Group {
HStack {
TextField("first name", text: $vm.firstName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Use width: 214
Text(vm.firstNameValidation)
}
HStack {
TextField("last name", text: $vm.lastName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(vm.lastNameValidation)
}
}
.padding()
}
.font(.title)
See how the 2 pipelines are stored…
}
}
www.bigmountainstudio.com
79
Combine Mastery in SwiftUI
Cancelling Multiple Pipelines
Store(in:) - View Model
class CancellingMultiplePipelinesViewModel: ObservableObject {
@Published var firstName: String = ""
@Published var firstNameValidation: String = ""
@Published var lastName: String = ""
@Published var lastNameValidation: String = ""
A Set is a little different from an array in that it only allows
unique elements. It will not allow duplicates.
It’s also good to keep in mind that a Set is unordered. So you
can’t guarantee the order of the cancellables you add to it.
private var validationCancellables: Set<AnyCancellable> = []
init() {
$firstName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.firstNameValidation = value
}
The sink subscriber returns an AnyCancellable but instead of
assigning it to a single property, as you saw before, it will be
passed down the pipeline to the store function which will add it to
a set of AnyCancellable types.
.store(in: &validationCancellables)
$lastName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.lastNameValidation = value
}
.store(in: &validationCancellables)
}
}
www.bigmountainstudio.com
80
Combine Mastery in SwiftUI
􀎷
Cancelling Multiple Pipelines
Cancel All Pipelines - View
struct CancelAllPipelines: View {
@StateObject private var vm = CancelAllPipelinesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Cancel All Pipelines",
subtitle: "RemoveAll",
desc: "You learned earlier that you can cancel one pipeline by calling
the cancel() function of the AnyCancellable. When everything is in
a Set, an easy way to cancel all pipelines is to simply remove all
of them from the Set.")
Group {
HStack {
TextField("first name", text: $vm.firstName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(vm.firstNameValidation)
}
HStack {
TextField("last name", text: $vm.lastName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(vm.lastNameValidation)
}
}
.padding()
Button("Cancel All Validations") {
vm.cancelAllValidations()
}
}
.font(.title)
}
Let’s see what vm.cancelAllValidations()
is actually doing.
}
www.bigmountainstudio.com
Once the validation pipelines are cancelled, the
validations no longer take place.
81
Combine Mastery in SwiftUI
Cancelling Multiple Pipelines
Cancel All Pipelines - View Model
class CancelAllPipelinesViewModel: ObservableObject {
@Published var firstName: String = ""
@Published var firstNameValidation: String = ""
@Published var lastName: String = ""
@Published var lastNameValidation: String = ""
private var validationCancellables: Set<AnyCancellable> = []
init() {
$firstName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.firstNameValidation = value
}
.store(in: &validationCancellables)
Just by removing an AnyCancellable reference,
a pipeline no longer has a place in memory and
will become deallocated.
$lastName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.lastNameValidation = value
}
.store(in: &validationCancellables)
This means that the subscription (sink) is
immediately cancelled and the publisher
($firstName, $lastName) will no longer
publish data changes.
}
func cancelAllValidations() {
validationCancellables.removeAll()
}
(Data doesn’t get published if no one is
subscribing to it.)
}
www.bigmountainstudio.com
82
Combine Mastery in SwiftUI
SUMMARY
You just learned the two most common subscribers that this book will be using for all of the Combine examples:
• assign(to: )
• sink(receivedValue: )
These subscribers will most likely be the ones that you use the most as well.
There’s a little bit more you can do with the sink subscriber. But for now, I wanted to get you used to creating and working with your first pipelines.
Summary
Where to go from here…
The first part of this book was to give you a conceptual understanding of Combine, architecture, important Swift language features related to Combine, and finally,
how to use Combine in a SwiftUI app with the @Published property wrapper and some subscribers. You have enough now to continue to the other parts of the book:
Publishers
Data from a URL
Operators
Subscribers
You don’t have to just use
@Published properties as
publishers.
Many apps get images or data
from a URL. The data received is
in JSON format and needs to be
converted into a more usable
format for your app.
There is probably an operator
for everything you do today
when handling data.
You learned about one
subscriber and I’m sure you will
use this one a lot. But
sometimes your pipeline will
handle data that doesn’t end by
being assigned to a @Published
property.
Learn other options here.
Did you know there are even
publishers built into some data
types now?
Organizing
Your pipelines, from publisher to
subscriber, don’t always have to
be fully assembled when you use
them.
Discover storing pieces of the
pipeline in functions or
properties later.
www.bigmountainstudio.com
Explore the available operators
and learn how to use them with
real SwiftUI examples.
Learn how to do this easily with
Combine.
Working with
Multiple Publishers
In plumbing, you need to
connect multiple pipes together
to deliver water to different
places or to merge hot and cold
water together.
You can do the same thing in
Combine!
84
Handling Errors
Debugging
You will most likely want to catch
and handle errors in your
pipeline before using assign.
Your pipelines aren’t always
going to run perfectly when
you’re constructing them.
Learn how to use the catch
operator to return something
your app can work with.
Learn tips, tricks, and operators
to assist you in understanding
what is happening in your
pipeline.
Combine Mastery in SwiftUI
PUBLISHERS
@Published Property
Property
$Pipeline
For a SwiftUI app, this will be your main publisher. It will publish values automatically to your views. But it also has a built-in publisher that you can attach a pipeline
to and have more logic run when values come down the pipeline (meaning a new value is assigned to the property).
􀎷
Publishers
@Published - View
struct Published_Introduction: View {
@StateObject private var vm = Published_IntroductionViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Published",
subtitle: "Introduction",
desc: "The @Published property wrapper has a built-in publisher that you
can access with the dollar sign ($).")
TextEditor(text: $vm.data)
.border(Color.gray, width: 1)
.frame(height: 200)
.padding()
Text("\(vm.characterCount)/\(vm.characterLimit)")
.foregroundColor(vm.countColor)
}
Combine is being used to produce the
character count as well as the color for
the text.
When the character count is above 24, the
color turns yellow, and above 30 is red.
.font(.title)
}
}
www.bigmountainstudio.com
87
Combine Mastery in SwiftUI
Publishers
@Published - View Model
class Published_IntroductionViewModel: ObservableObject {
var characterLimit = 30
@Published var data = ""
@Published var characterCount = 0
@Published var countColor = Color.gray
init() {
$data
.map { data -> Int in
return data.count
Use the dollar sign ($) to access the @Published
property’s publisher. From here you can create a
pipeline so every time the property changes, this
pipeline will run.
When the data property changes I get the character
count and assign it to another @Published
property.
}
.assign(to: &$characterCount)
$characterCount
.map { [unowned self] count -> Color in
let eightyPercent = Int(Double(characterLimit) * 0.8)
if (eightyPercent...characterLimit).contains(count) {
return Color.yellow
I also have a pipeline on the
characterCount so when
it changes, I figure out the
color to use for the text on
the view.
Use width: 214
} else if count > characterLimit {
return Color.red
}
return Color.gray
}
.assign(to: &$countColor)
}
}
www.bigmountainstudio.com
88
Combine Mastery in SwiftUI
CurrentValueSubject
This publisher is used mainly in non-SwiftUI apps but you might have a need for it at some point. In many ways, this publisher works like @Published properties (or
rather, @Published properties work like the CurrentValueSubject publisher).
It’s a publisher that holds on to a value (current value) and when the value changes, it is published and sent down a pipeline when there are subscribers attached to
the pipeline.
If you are going to use this with SwiftUI then there is an extra step you will have to take so the SwiftUI view is notified of changes.
Publishers
CurrentValueSubject - Declaring
var subject: CurrentValueSubject<String, Never>
The type you want to store in this
property (more specifically, the type
that will be sent to the subscriber).
This is the error that could be sent to the subscriber if something goes
wrong. Never means the subscriber should not expect an error/failure.
Otherwise, you can create your own custom error and set this type.
var subject = CurrentValueSubject<Bool, Never>(false)
You can send in the value directly into the initializer too.
(The type should match the first type you specify.)
(Note: If any of this use of generics is looking unfamiliar to you, then take a look at the chapter on Generics and how they are used with Combine.)
www.bigmountainstudio.com
90
Combine Mastery in SwiftUI
Publishers
CurrentValueSubject - View
struct CurrentValueSubject_Intro: View {
@StateObject private var vm = CurrentValueSubjectViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("CurrentValueSubject",
subtitle: "Introduction",
desc: "The CurrentValueSubject publisher will publish its existing value
and also new values when it gets them.")
Button("Select Lorenzo") {
vm.selection.send("Lorenzo")
}
Use width: 214
Button("Select Ellen") {
vm.selection.value = "Ellen"
The idea here is we want to make the text red if they
select the same thing twice.
But there is a problem that has to do with when a
CurrentValueSubject’s pipeline is run.
See view model on next page…
}
Text(vm.selection.value)
.foregroundColor(vm.selectionSame.value ? .red : .green)
}
.font(.title)
}
}
Notice that you have to access the value property to
read the publisher’s underlying value.
www.bigmountainstudio.com
91
Combine Mastery in SwiftUI
Publishers
CurrentValueSubject - Setting Values
View Model
In the view model (which you will see on the next page) the selection property is declared as a CurrentValueSubject like this:
var selection = CurrentValueSubject<String, Never>("No Name Selected")
View
In the view, you may have noticed that I’m setting the selection publisher’s underlying value in TWO different ways:
Button("Select Lorenzo") {
vm.selection.send("Lorenzo")
}
Using the send function or setting value directly are both valid.
In Apple’s documentation it says:
Button("Select Ellen") {
vm.selection.value = "Ellen"
}
“Calling send(_:) on a CurrentValueSubject also updates the current value, making it
equivalent to updating the value directly.”
Personally, I think I would prefer to call the send function because it’s kind of like
saying, “Send a value through the pipeline to the subscriber.”
www.bigmountainstudio.com
92
Combine Mastery in SwiftUI
Publishers
CurrentValueSubject - View Model
class CurrentValueSubjectViewModel: ObservableObject {
var selection = CurrentValueSubject<String, Never>("No Name Selected")
var selectionSame = CurrentValueSubject<Bool, Never>(false)
Pipeline: Compares the previous value with the new
value and returns true if they are the same.
var cancellables: [AnyCancellable] = []
init() {
selection
.map{ [unowned self] newValue -> Bool in
if newValue == selection.value {
return true
} else {
return false
This will NOT work.
The newValue will ALWAYS equal
the current value.
Unlike @Published properties,
this pipeline runs AFTER the
current value has been set.
}
}
.sink { [unowned self] value in
selectionSame.value = value
objectWillChange.send()
Note: This whole if block could be shortened to
just:
newValue == selection
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
This part is super important. Without
this, the view will not know to update.
As a test, comment out this line and you
will notice the view never gets notified of
changes.
93
Combine Mastery in SwiftUI
Publishers
CurrentValueSubject Compared with @Published
Sequence of Events
CurrentValueSubject
@Published
1
The value is set
1
The pipeline is run
2
The pipeline is run
2
The value is set
3
The UI is notified of changes (using objectWillChange.send())
3
The UI is automatically notified of changes
Let’s see how the same UI and view model would work if we used
@Published properties instead of a CurrentValueSubject publisher.
www.bigmountainstudio.com
94
Combine Mastery in SwiftUI
􀎷
Publishers
CurrentValueSubject Compared - View
struct CurrentValueSubject_Compared: View {
@StateObject private var vm = CurrentValueSubject_ComparedViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("CurrentValueSubject",
subtitle: "Compared",
desc: "Let's compare with @Published. The map operator will work now
because the @Published property's value doesn't actually change
until AFTER the pipeline has finished.")
Button("Select Lorenzo") {
vm.selection = "Lorenzo"
}
Button("Select Ellen") {
vm.selection = "Ellen"
The view model for this view is using a
@Published property for just the
selection property.
So you will notice we set it normally here.
}
Text(vm.selection)
.foregroundColor(vm.selectionSame.value ? .red : .green)
}
.font(.title)
}
}
www.bigmountainstudio.com
95
Combine Mastery in SwiftUI
Publishers
CurrentValueSubject Compared - View Model
class CurrentValueSubject_ComparedViewModel: ObservableObject {
The only thing that has changed is the selection
property is now using the @Published property
wrapper instead of being a CurrentValueSubject
publisher.
@Published var selection = "No Name Selected"
var selectionSame = CurrentValueSubject<Bool, Never>(false)
var cancellables: [AnyCancellable] = []
init() {
$selection
.map{ [unowned self] newValue -> Bool in
This will work now!
The selection property will still have the PREVIOUS value.
if newValue == selection {
return true
} else {
Remember the sequence for @Published properties:
return false
}
}
.sink { [unowned self] value in
selectionSame.value = value
objectWillChange.send()
}
.store(in: &cancellables)
1. The pipeline is run
2. The value is set
3. The UI is automatically notified of changes
So the selection property is only updated AFTER the pipeline has
run first which allows us to inspect the previous value.
}
}
You still need objectWillChange.send()
because the value is still being assigned to a
CurrentValueSubject.
www.bigmountainstudio.com
96
Combine Mastery in SwiftUI
Empty
“Last Item”
In SwiftUI you might be familiar with the EmptyView. Well, Combine has an Empty publisher. It is simply a publisher that publishes nothing. You can have it finish
immediately or fail immediately. You can also have it never complete and just keep the pipeline open.
When would you want to use this? One scenario that comes to mind is when doing error handling with the catch operator. Using the catch operator you can
intercept all errors coming down from an upstream publisher and replace them with another publisher. So if you don’t want another value to be published you can
use an Empty publisher instead. Take a look at this example on the following pages.
Publishers
Empty - View
struct Empty_Intro: View {
@StateObject private var vm = Empty_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Empty",
subtitle: "Introduction",
desc: "The Empty publisher will send nothing down the pipeline.")
List(vm.dataToView, id: \.self) { item in
Text(item)
Use width: 214
}
DescView("The item after Value 3 caused an error. The Empty publisher was then used
and the pipeline finished immediately.")
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
98
Combine Mastery in SwiftUI
Publishers
Empty - View Model
class Empty_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"]
_ = dataIn.publisher
.tryMap{ item in
The tryMap operator gives you a closure to run some code for each item that
comes through the pipeline with the option of also throwing an error.
if item == "🧨 " {
throw BombDetectedError()
}
return item
}
.catch { (error) in
Empty(completeImmediately: true)
In this example, the Empty publisher is used to end a pipeline immediately after an
error is caught. The catch operator is used to intercept errors and supply another
publisher.
Note: I didn’t have to explicitly set the completeImmediately parameter to true
because that is the default value.
}
.sink { [unowned self] (item) in
dataToView.append(item)
}
}
}
www.bigmountainstudio.com
99
Combine Mastery in SwiftUI
Fail
!
Error
As you might be able to guess from the name, Fail is a publisher that publishes a failure (with an error). Why would you need this? Well, you can put publishers inside
of properties and functions. And within the property getter or the function body, you can evaluate input. If the input is valid, return a publisher, else return a Fail
publisher. The Fail publisher will let your subscriber know that something failed. You will see an example of this on the following pages.
Publishers
Fail - View
struct Fail_Intro: View {
@StateObject private var vm = Fail_IntroViewModel()
@State private var age = ""
var body: some View {
VStack(spacing: 20) {
HeaderView("Fail",
subtitle: "Introduction",
desc: "The Fail publisher will simply publish a failure with your error
and close the pipeline.")
TextField("Enter Age", text: $age)
.keyboardType(UIKeyboardType.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
Use width: 214
.padding()
Button("Save") {
vm.save(age: Int(age) ?? -1)
}
When you tap Save, a save function on the view
model is called. The age is validated and if not
between 1 and 100 the Fail publisher is used.
See how this is done on the next page.
Text("\(vm.age)")
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Invalid Age"), message: Text(error.rawValue))
}
}
}
www.bigmountainstudio.com
101
Combine Mastery in SwiftUI
Publishers
Fail - View Model
class Validators {
static func validAgePublisher(age: Int) -> AnyPublisher<Int, InvalidAgeError> {
if age < 0 {
return Fail(error: InvalidAgeError.lessThanZero)
.eraseToAnyPublisher()
} else if age > 100 {
return Fail(error: InvalidAgeError.moreThanOneHundred)
.eraseToAnyPublisher()
}
This function can return different publisher
types. Luckily, we can use
eraseToAnyPublisher to make them all a
common type of publisher that returns an Int
or an InvalidAgeError as its failure type.
Learn more about AnyPublisher and
organizing pipelines.
return Just(age)
.setFailureType(to: InvalidAgeError.self)
.eraseToAnyPublisher()
}
}
class Fail_IntroViewModel: ObservableObject {
@Published var age = 0
@Published var error: InvalidAgeError?
func save(age: Int) {
_ = Validators.validAgePublisher(age: age)
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = error
}
} receiveValue: { [unowned self] age in
self.age = age
}
}
Normally, the Just publisher doesn’t throw errors. So we have to use setFailureType
so we can match up the failure types of our Fail publishers above.
This allows us to use eraseToAnyPublisher so all Fail and this Just publisher are all
the same type that we return from this function.
If validAgePublisher returns a Fail
publisher then the sink completion will catch
it and the error is assigned to the error
@Published property.
Or else the Just publisher is returned and
the age is used.
}
www.bigmountainstudio.com
102
Learn more about error-throwing
and non-error-throwing pipelines
in the Handling Errors chapter.
Combine Mastery in SwiftUI
Future
The Future publisher will publish only one value and then the pipeline will close. WHEN the value is published is up to you. It can publish immediately, be delayed,
wait for a user response, etc. But one thing to know about Future is that it ONLY runs one time. You can use the same Future with multiple subscribers. But it still
only executes its closure one time and stores the one value it is responsible for publishing. You will see examples on the following pages.
Publishers
Future - Declaring
var futurePublisher: Future<String, Never>
The type you want to pass down the
pipeline in the future to the
subscriber.
This is the error that could be sent to the subscriber if something goes
wrong. Never means the subscriber should not expect an error/failure.
Otherwise, you can create your own custom error and set this type.
let futurePublisher = Future<String, Never> { promise in
What is Result?
Result is an enum with two
cases: success and failure.
promise(Result.success("👋 "))
}
The promise parameter passed into the closure is
actually a function definition. The function looks like this:
promise(Result<String, Never>) -> Void
You can assign a value to each
one. The value is a generic so
you can assign a String, Bool,
Int, or any other type to them.
You want to call this function at some point in the future’s
closure.
In this example, a String is being
assigned to the success case.
(Note: If any of this use of generics is looking unfamiliar to you, then take a look at the chapter on Generics and how they are used with Combine.)
www.bigmountainstudio.com
104
Combine Mastery in SwiftUI
Publishers
Future - View
struct Future_Intro: View {
@StateObject private var vm = Future_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Future",
subtitle: "Introduction",
desc: "The future publisher will publish one value, either immediately or
at some future time, from the closure provided to you.")
Button("Say Hello") {
vm.sayHello()
}
Use width: 214
Text(vm.hello)
.padding(.bottom)
Button("Say Goodbye") {
vm.sayGoodbye()
}
Text(vm.goodbye)
Spacer()
In this example, the sayHello function will
immediately return a value.
The sayGoodbye function will be delayed
before returning a value.
}
.font(.title)
}
}
www.bigmountainstudio.com
105
Combine Mastery in SwiftUI
Publishers
Future - View Model
class Future_IntroViewModel: ObservableObject {
@Published var hello = ""
In this example, a new Future publisher is being
created and returning one value, “Hello, World!”.
@Published var goodbye = ""
var goodbyeCancellable: AnyCancellable?
func sayHello() {
Because Future is declared with no possible failure (Never), this becomes a non-errorthrowing pipeline.
We don’t need sink(receiveCompletion:receiveValue:) to look for and handle
errors. So, assign(to:) can be used.
Future<String, Never> { promise in
promise(Result.success("Hello, World!"))
}
.assign(to: &$hello)
(See chapter on Handling Errors to learn more.)
}
func sayGoodbye() {
let futurePublisher = Future<String, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise(.success("Goodbye, my friend 👋 "))
Here is an example of where the Future publisher is being assigned to a variable.
Within it, there is a delay of some kind but there is still a promise that either a
success or failure will be published. (Notice Result isn’t needed.)
}
}
goodbyeCancellable = futurePublisher
.sink { [unowned self] message in
goodbye = message
}
}
}
www.bigmountainstudio.com
This pipeline is also non-error-throwing but instead of using assign(to:), sink is used.
(You could just as easily use assign(to:) here.)
Also, there are two reasons why this pipeline is being assigned to an AnyCancellable:
1. Because there is a delay within the future’s closure, the pipeline will get deallocated as soon
as it goes out of the scope of this function - BEFORE a value is returned.
2. The sink subscriber returns AnyCancellable. If assign(to:) was used, then this would
not be needed.
106
Combine Mastery in SwiftUI
Publishers
Future - Immediate Execution
class Future_ImmediateExecutionViewModel: ObservableObject {
@Published var data = ""
This is the view model.
func fetch() {
_ = Future<String, Never> { [unowned self] promise in
data = "Hello, my friend 👋 "
}
This Future publisher has no subscriber, yet as
soon as it is created it will publish immediately.
}
}
struct Future_ImmediateExecution: View {
@StateObject private var vm = Future_ImmediateExecutionViewModel()
var body: some View {
Use width: 214
VStack(spacing: 20) {
HeaderView("Future",
subtitle: "Immediate Execution",
desc: "Future publishers execute immediately, whether they have a
subscriber or not. This is different from all other publishers.")
Text(vm.data)
}
.font(.title)
Note: I do not recommend using this publisher this way. This
is simply to demonstrate that the Future publisher will
publish immediately, whether it has a subscriber or not.
.onAppear {
vm.fetch()
}
}
I’m pretty sure Apple doesn’t intend it to be used this way.
}
www.bigmountainstudio.com
107
Combine Mastery in SwiftUI
Publishers
Future - Only Runs Once - View
struct Future_OnlyRunsOnce: View {
@StateObject private var vm = Future_OnlyRunsOnceViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Future",
subtitle: "Only Runs Once",
desc: "Another thing that sets the Future publisher apart is that it only
runs one time. It will store its value after being run and then
never run again.")
Text(vm.firstResult)
Use width: 214
Button("Run Again") {
vm.runAgain()
}
No matter how many times you tap this button,
the Future publisher will not execute again.
See view model on next page…
Text(vm.secondResult)
}
.font(.title)
.onAppear {
This is the first time the Future is getting used.
When the “Run Again” button is tapped, the same
future is reused.
vm.fetch()
}
}
}
www.bigmountainstudio.com
108
Combine Mastery in SwiftUI
Publishers
Future - Only Runs Once - View Model
class Future_OnlyRunsOnceViewModel: ObservableObject {
@Published var firstResult = ""
@Published var secondResult = ""
let futurePublisher = Future<String, Never> { promise in
promise(.success("Future Publisher has run! 🙌 "))
print("Future Publisher has run! 🙌 ")
You will see this printed in the Xcode Debugger Console only one time.
}
func fetch() {
futurePublisher
.assign(to: &$firstResult)
}
func runAgain() {
futurePublisher
.assign(to: &$secondResult)
}
}
This function can be run repeatedly and the
futurePublisher will emit the same, original value,
every single time but will not actually get executed.
www.bigmountainstudio.com
109
So what if you don’t want the Future publisher to execute
immediately when created? What can you do?
We look at wrapping a Future with another publisher to help
with this on the next page.
Combine Mastery in SwiftUI
Publishers
Future - Run Multiple Times (Deferred) - View
struct Future_RunMultipleTimes: View {
@StateObject private var vm = Future_RunMultipleTimesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Future",
subtitle: "Run Multiple Times",
desc: "Future publishers execute one time and execute immediately. To
change this behavior you can use the Deferred publisher which will
wait until a subscriber is attached before letting the Future
execute and publish.")
Text(vm.firstResult)
Use width: 214
Button("Run Again") {
The word “defer” means to “postpone some activity
or event to a later time”. In this case, putting off
executing the Future until it is needed.
vm.runAgain()
}
Text(vm.secondResult)
Using the Deferred publisher, the Future publisher
will execute every time this button is tapped.
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
This view and view model are almost exactly the same as the previous example.
There is one small change in the view model, which you will see on the next page.
110
Combine Mastery in SwiftUI
Publishers
Future - Run Multiple Times (Deferred) - View Model
class Future_RunMultipleTimesViewModel: ObservableObject {
@Published var firstResult = ""
@Published var secondResult = ""
The Deferred publisher is pretty simple to implement. You just put another
publisher within it like this.
let futurePublisher = Deferred {
Future<String, Never> { promise in
The Future publisher will not execute immediately now when it is created
because it is inside the Deferred publisher. Even more, it will execute every
time a subscriber is attached.
promise(.success("Future Publisher has run! 🙌 "))
print("Future Publisher has run! 🙌 ")
}
}
func fetch() {
futurePublisher
.assign(to: &$firstResult)
}
func runAgain() {
futurePublisher
.assign(to: &$secondResult)
}
}
This function can be run repeatedly and the
futurePublisher will now get executed every time.
www.bigmountainstudio.com
111
Note: I am not sure what else to use the Deferred publisher
with because the Future publisher is the only one I know that
executes immediately. All the other publishers I know of do
not publish unless a subscriber is attached.
Combine Mastery in SwiftUI
Publishers
Deferred-Future Pattern for Existing APIs
Turn existing API calls into Publishers
I just wanted to mention quickly that this Deferred { Future { … } } pattern is a great way to wrap APIs that are not converted to use Combine
publishers. This means you could wrap your data store calls with this pattern and then be able to attach operators and sinks to them.
You can also use it for many of Apple’s Kits where you need to get information from a device, or ask the user for permissions to access something, like
photos, or other private or sensitive information.
newApiPublisher
=
Deferred
Future
Successful Operation
promise(.success(<Some Type>))
Failed Operation
promise(.failure(<Some Error>))
www.bigmountainstudio.com
112
Combine Mastery in SwiftUI
Just
Using the Just publisher can turn any variable into a publisher. It will take any value you have and send it through a pipeline that you attach to it one time and then
finish (stop) the pipeline.
(“Just” in this case means, “simply, only or no more than one”.)
Publishers
Just - View
struct Just_Introduction: View {
@StateObject private var vm = Just_IntroductionViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Just",
subtitle: "Introduction",
desc: "The Just publisher can turn any object into a publisher if it
doesn't already have one built-in. This means you can attach
pipelines to any property or value.")
.layoutPriority(1)
Text("This week's winner:")
Text(vm.data)
.bold()
Use width: 214
Form {
Section(header: Text("Contest Participants").padding()) {
List(vm.dataToView, id: \.self) { item in
Text(item)
}
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
In this example, the Just publisher is being used to
publish just the first element in the array of results
and capitalizing it and then assigning it to a published
property on the observable object.
}
}
www.bigmountainstudio.com
114
Combine Mastery in SwiftUI
Publishers
Just - View Model
class Just_IntroductionViewModel: ObservableObject {
@Published var data = ""
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Julian", "Meredith", "Luan", "Daniel", "Marina"]
_ = dataIn.publisher
.sink { [unowned self] (item) in
dataToView.append(item)
}
if dataIn.count > 0 {
Just(dataIn[0])
.map { item in
item.uppercased()
}
.assign(to: &$data)
}
}
You can see in this chapter that Apple added
built-in publishers to many existing types. For
everything else, there is Just.
It may not seem like a lot but being able to start
a pipeline quickly and easily this way opens the
door to all the operators you can apply to the
pipeline.
After Just publishes the one item, it will finish
the pipeline.
}
www.bigmountainstudio.com
115
Combine Mastery in SwiftUI
PassthroughSubject
The PassthroughSubject is much like the CurrentValueSubject except this publisher does NOT hold on to a value. It simply allows you to create a pipeline
that you can send values through.
This makes it ideal to send “events” from the view to the view model. You can pass values through the PassthroughSubject and right into a pipeline as you will see on
the following pages.
􀎷
Publishers
PassthroughSubject - View
struct PassthroughSubject_Intro: View {
@StateObject private var vm = PassthroughSubjectViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("PassthroughSubject",
subtitle: "Introduction",
desc: "The PassthroughSubject publisher will send a value through a
pipeline but not retain the value.")
HStack {
TextField("credit card number", text: $vm.creditCard)
Group {
switch (vm.status) {
case .ok:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
case .invalid:
Image(systemName: "x.circle.fill")
A PassthroughSubject is a good
.foregroundColor(.red)
candidate when you need to send a
default:
EmptyView()
value through a pipeline but don’t
}
necessarily need to hold on to that
}
value.
}
.padding()
Button("Verify CC Number") {
vm.verifyCreditCard.send(vm.creditCard)
}
I use it here to validate a value when a
button is tapped.
}
.font(.title)
}
Like the CurrentValueSubject, you
have access to a send function that will
send the value through your pipeline.
}
www.bigmountainstudio.com
117
Combine Mastery in SwiftUI
Publishers
PassthroughSubject - View Model
enum CreditCardStatus {
case ok
case invalid
case notEvaluated
}
The UI shows an
image based on
the credit card
status.
Pipeline: The idea here is that a credit card number is
checked to see if it’s 16 digits. The status property is updated
with the result.
class PassthroughSubjectViewModel: ObservableObject {
This Passthrough publisher will not
retain a value. It simply expects a
String.
@Published var creditCard = ""
@Published var status = CreditCardStatus.notEvaluated
let verifyCreditCard = PassthroughSubject<String, Never>()
If there is a subscriber attached to it
then it will send any received values
through the pipeline to the subscriber.
init() {
verifyCreditCard
.map{ creditCard -> CreditCardStatus in
if creditCard.count == 16 {
return CreditCardStatus.ok
The verifyCreditCard publisher is specified to receive a
String and not return any error:
} else {
return CreditCardStatus.invalid
PassthroughSubject<String, Never>
}
}
Without doing anything, the pipeline expects a String will go
all the way through. But you can change this.
.assign(to: &$status)
}
}
And that’s what is happening here. The map operator now
returns an enum CreditCardStatus and we store the result
in the status property.
Remember, when using the assign(to:) subscriber, there is no
need to store a reference to this pipeline (AnyCancellable).
www.bigmountainstudio.com
118
Combine Mastery in SwiftUI
Sequence
[Item10,
Item9,
Item8,
Item7,
Item6...]
I
Item5
Item4
Item3
m
te
1
Item2
There are types in Swift have built-in publishers. In this section, you will learn about the Sequence publisher which sends elements of a collection through a pipeline
one at a time.
Once all items have been sent through the pipeline, it finishes. No more items will go through, even if you add more items to the collection later.
Publishers
Sequence - View
struct Sequence_Intro: View {
@StateObject private var vm = SequenceIntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Sequence",
subtitle: "Introduction",
desc: "Arrays have a built-in sequence publisher property. This means a
pipeline can be constructed right on the array.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
Use width: 214
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
Many data types in Swift now have built-in
publishers, including arrays.
See view model on next page…
www.bigmountainstudio.com
120
Combine Mastery in SwiftUI
Publishers
Sequence - View Model
class SequenceIntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
var dataIn = ["Paul", "Lem", "Scott", "Chris", "Kaya", "Mark", "Adam", "Jared"]
// Process values
dataIn.publisher
.sink(receiveCompletion: { (completion) in
print(completion)
(Xcode Debugger Console)
}, receiveValue: { [unowned self] datum in
self.dataToView.append(datum)
print(datum)
})
.store(in: &cancellables)
Notice if you try to add more to the sequence
later, the pipeline will not execute.
// These values will NOT go through the pipeline.
// The pipeline finishes after publishing the initial set.
As soon as the initial sequence was published it
was automatically finished as you can see with
the print statement in the receiveCompletion
closure.
dataIn.append(contentsOf: ["Rod", "Sean", "Karin"])
}
}
www.bigmountainstudio.com
121
Combine Mastery in SwiftUI
Publishers
Sequence - The Type
If you hold down OPTION and click on publisher, you will see the type:
Notice the input type is [String], not String.
This means the array is passed into the publisher and the
publisher iterates through all items in the array (and then
the publisher finishes).
Strings also have a Sequence publisher built
into them.
How would this work?
www.bigmountainstudio.com
122
Combine Mastery in SwiftUI
Publishers
Sequence - With String
class Sequence_StringViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
let dataIn = "Hello, World!"
dataIn.publisher
.sink { [unowned self] datum in
self.dataToView.append(String(datum))
print(datum)
}
.store(in: &cancellables)
}
If you need to iterate over
each character in a String,
you can use its publisher
property.
}
Use width: 214
struct Sequence_String: View {
@StateObject private var vm = Sequence_StringViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Sequence",
subtitle: "With String",
desc: "When using a Sequence publisher on a String, it will treat
each character as an item in a collection.”)
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
.onAppear { vm.fetch() }
}
}
www.bigmountainstudio.com
123
Combine Mastery in SwiftUI
Timer
9:17
9:16
9:15
9:14
9:13
9:12
9:11
9:10
9:09
9:08
9:07
The Timer publisher repeatedly publishes the current date and time with an interval that you specify. So you can set it up to publish the current date and time every
5 seconds or every minute, etc.
You may not necessarily use the date and time that’s published but you could attach operators to run some code at an interval that you specify using this publisher.
􀎷
Publishers
Timer - View
struct Timer_Intro: View {
@StateObject var vm = TimerIntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Timer",
subtitle: "Introduction",
desc: "The Timer continually publishes the updated date and time at an
interval you specify.")
Text("Adjust Interval")
Slider(value: $vm.interval, in: 0.1...1,
minimumValueLabel: Image(systemName: "hare"),
maximumValueLabel: Image(systemName: "tortoise"),
label: { Text(“Interval") })
.padding(.horizontal)
List(vm.data, id: \.self) { datum in
Text(datum)
.font(.system(.title, design: .monospaced))
}
}
.font(.title)
.onAppear {
vm.start()
The Timer publisher will be using the interval you are setting
with this Slider view.
The shorter the interval, the faster the Timer publishes items.
}
}
}
www.bigmountainstudio.com
125
Combine Mastery in SwiftUI
Publishers
Timer - View Model
class TimerIntroViewModel: ObservableObject {
@Published var data: [String] = []
@Published var interval: Double = 1
private var timerCancellable: AnyCancellable?
private var intervalCancellable: AnyCancellable?
I created another pipeline on the interval
published property so when it changes value,
I can restart the timer’s pipeline so it
reinitializes with the new interval value.
let timeFormatter = DateFormatter()
Learn more
about dropFirst
init() {
timeFormatter.dateFormat = "HH:mm:ss.SSS"
intervalCancellable = $interval
.dropFirst()
.sink { [unowned self] interval in
// Restart the timer pipeline
timerCancellable?.cancel()
data.removeAll()
start()
}
You set the Timer’s interval
with the publish modifier.
For the on parameter, I
set .main to have this run on
the main thread.
Use width: 214
}
The last parameter is the
RunLoop mode.
(Run loops manage events and
work and allow multiple things
to happen simultaneously.)
In almost all cases you will
just use the common run loop.
func start() {
timerCancellable = Timer
.publish(every: interval, on: .main, in: .common)
.autoconnect()
.sink{ [unowned self] (datum) in
data.append(timeFormatter.string(from: datum))
}
}
}
The autoconnect operator seen here allows the Timer to automatically start publishing items.
www.bigmountainstudio.com
126
Combine Mastery in SwiftUI
􀎷
Publishers
Timer Connect - View
struct Timer_Connect: View {
@StateObject private var vm = Timer_ConnectViewModel()
var body: some View {
VStack(spacing: 20) {
On the previous page, you saw that
the autoconnect operator allowed
the Timer to publish data right away.
Without it, the Timer will not publish.
HeaderView("Timer",
subtitle: "Connect",
desc: "Instead of using autoconnect, you can manually connect the Timer
publisher which is like turning on the flow of water.")
HStack {
Button("Connect") { vm.start() }
.frame(maxWidth: .infinity)
Button("Stop") { vm.stop() }
In this example, when the Connect
button is tapped it will call the
connect function manually and allow
the Timer to start publishing.
.frame(maxWidth: .infinity)
}
List(vm.data, id: \.self) { datum in
Text(datum)
.font(.system(.title, design: .monospaced))
}
}
.font(.title)
}
}
www.bigmountainstudio.com
127
Combine Mastery in SwiftUI
Publishers
Timer Connect - View Model
class Timer_ConnectViewModel: ObservableObject {
@Published var data: [String] = []
private var timerPublisher = Timer.publish(every: 0.2, on: .main, in: .common)
private var timerCancellable: Cancellable?
private var cancellables: Set<AnyCancellable> = []
let timeFormatter = DateFormatter()
I separate the publisher and
subscriber because the connect
function will only work on the
publisher itself.
init() {
timeFormatter.dateFormat = "HH:mm:ss.SSS"
timerPublisher
.sink { [unowned self] (datum) in
data.append(timeFormatter.string(from: datum))
}
.store(in: &cancellables)
}
func start() {
timerCancellable = timerPublisher.connect()
}
When the connect function is called,
the Timer will start to publish.
Use width: 214
Note: The connect function ONLY
works on the publisher itself. So you
will have to separate your subscriber
from your publisher as you see here.
func stop() {
timerCancellable?.cancel()
data.removeAll()
}
}
www.bigmountainstudio.com
The connect and autoconnect functions
are only available on publishers that conform
to the ConnectablePublisher protocol, like
the Timer.
128
Combine Mastery in SwiftUI
URLSession’s DataTaskPublisher
error
https://...
(
,
)
If you need to get data from an URL then URLSession is the object you want to use. It has a DataTaskPublisher that is actually a publisher which means you can send
the results of a URL API call down a pipeline and process it and eventually assign the results to a property.
There is a lot involved so before diving into code, I’m going to show you some of the major parts and describe them.
Publishers
URLSession
I want to give you a brief overview of URLSession so you at least have an idea of what it is in case you have never used it before. You will learn just enough to get
data from a URL and then we will focus on how that data gets published and send down a pipeline.
There are many things you can do with a URLSession and many ways you can configure it for different situations. This is beyond the scope of this book.
Data task (fetch)
URLSession
Download task
Upload task
The URLSession is an object that you use for:
• Downloading data from a URL endpoint
• Uploading data from a URL endpoint
• Performing background downloads when your app isn’t running
• Coordinating multiple tasks
www.bigmountainstudio.com
130
Combine Mastery in SwiftUI
Publishers
URLSession.shared
The URLSession has a shared property that is a singleton. That basically means you don’t have to instantiate the URLSession and there is always only one
URLSession. You can use it multiple times to do many tasks (fetch, upload, download, etc.)
This is great for basic URL requests. But if you need more, you can instantiate the URLSession with more configuration options:
Basic
Advanced
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration)
URLSession.shared
Great for simple tasks like fetching data from a URL to memory
You can’t obtain data incrementally as it arrives from the server
You can’t customize the connection behavior
Your ability to perform authentication is limited
You can’t perform background downloads or uploads when your app
isn’t running
• You can’t customize caching, cookie storage, or credential storage
•
•
•
•
•
•
•
•
•
•
•
•
You can change the default request and response timeouts
You can make the session wait for connectivity to be established
You can prevent your app from using a cellular network
Add additional HTTP headers to all requests
Set cookie, security, and caching policies
Support background transfers
See more options here.
For the examples in this book, I will just be using URLSession.shared.
www.bigmountainstudio.com
131
Combine Mastery in SwiftUI
Publishers
URLSession.shared.DataTaskPublisher
The DataTaskPublisher will take a URL and then attempt to fetch data from it and publish the results.
URLSession
Creates
DataTaskPublisher
Can return
Data
Response
The data is what is returned from the URL
you provided to the DataTaskPublisher.
Note: What is returned is represented as
bytes in memory, not text or an image.
The response is like the status of how the
call to the URL went. Could it connect? Was
it successful? What kind of data was
returned?
www.bigmountainstudio.com
132
Error
If there was some problem with trying to
connect and get data then an error is
thrown.
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - View
struct UrlDataTaskPublisher_Intro: View {
@StateObject private var vm = UrlDataTaskPublisher_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("URLSession DataTaskPublisher",
subtitle: "Introduction",
desc: "URLSession has a dataTaskPublisher you can use to get data from a
URL and run it through a pipeline.")
List(vm.dataToView, id: \._id) { catFact in
Text(catFact.text)
}
Use width: 214
.font(.title3)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
There are a lot of different operators involved when it
comes to using the dataTaskPublisher. I am going to
start with this simple example and walk you through it.
}
This URL I’m using returns some cat facts. Let’s see how
the pipeline looks on the next page.
www.bigmountainstudio.com
133
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - View Model
struct CatFact: Decodable {
let _id: String
let text: String
}
Note: In order to keep this first example as simple as
possible, there are a lot of things I’m NOT doing, such as
checking for and handling errors. I’ll cover this in the
following pages.
Many more fields are
returned from the API but
we only care about two.
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
@Published var dataToView: [CatFact] = []
Remember, the dataTaskPublisher can return 3 things:
var cancellables: Set<AnyCancellable> = []
func fetch() {
DataTaskPublisher
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
Data
.decode(type: [CatFact].self, decoder: JSONDecoder())
Response
Error
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [unowned self] catFact in
The Data and Response can be inspected inside a map operator. Since
dataTaskPublisher returns these two things, the map operator will
automatically expose those two things as input parameters.
dataToView = catFact
})
.store(in: &cancellables)
}
If dataTaskPublisher throws an error then it’ll go straight to the sink’s
completion handler.
}
www.bigmountainstudio.com
134
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - Map
struct CatFact: Decodable {
The dataTaskPublisher publishes a tuple: Data & URLResponse.
(A tuple is a way to combine two values into one.)
This tuple will continue down the pipeline unless we specifically republish
a different type.
let _id: String
let text: String
}
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
Map
@Published var dataToView: [CatFact] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
And that is what we are doing with the map operator. The map receives
the tuple but then republishes only one value from the tuple.
(Note: The return keyword was made optional in Swift 5 if there is only one
thing being returned. You could use return data if it makes it more clear
for you.)
Can it be shorter?
data
Yes! I wanted to start with this format so you can explicitly see the tuple
coming in from the dataTaskPublisher.
}
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
To make this shorter you can use what’s called “shorthand argument
names” or “anonymous closure arguments”. It’s a way to reference
arguments coming into a closure with a dollar sign and numbers:
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [unowned self] catFact in
$0 = (data: Data, response: URLResponse)
dataToView = catFact
The $0 represents the tuple.
})
.store(in: &cancellables)
Using shorthand argument names, you can write the map like this:
}
}
www.bigmountainstudio.com
.map { $0.data }
135
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - Decode
struct CatFact: Decodable {
let _id: String
The map operator is now republishing just the data value we
received from dataTaskPublisher.
let text: String
}
What is Data?
The data value represents what we received from the URL
endpoint. It is just a bunch of bytes in memory that could
represent different things like text or an image. In order to use
data, we will have to transform or decode it into something else.
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
@Published var dataToView: [CatFact] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [CatFact].self, decoder: JSONDecoder())
Decode
The decode operator not only decodes those bytes into something
we can use but will also apply the decoded data into a type that
you specify.
Since you know you are getting back JSON (Javascript Object
Notation) from the URL endpoint, you can use the JSONDecoder.
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
We also know there is a “_id” field and a “text” field in that JSON so
we create a struct containing those two fields. In order for the
decode operator to work, we have to make that struct conform to
Decodable.
print(completion)
}, receiveValue: { [unowned self] catFact in
dataToView = catFact
})
But notice we’re not putting the data into one CatFact. We’re
putting the data into an array of CatFact objects.
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
136
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - Receive(on: )
struct CatFact: Decodable {
Asynchronous
let _id: String
The dataTaskPublisher will run asynchronously. This means that your app
will be doing multiple things at one time.
let text: String
}
While your app is getting data from a URL endpoint and decoding it in the
background, the user can still use your app and it’ll be responsive in the
foreground.
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
@Published var dataToView: [CatFact] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
But once you have your data you received all decoded and in a readable
format that you can present on a view, it’s time to switch over to the
foreground.
We call the background and foreground “threads” in memory.
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
Thread Switching
.decode(type: [CatFact].self, decoder: JSONDecoder())
To move data that is coming down your background pipeline to a new
foreground pipeline, you can use the receive(on:) operator.
It basically is saying, “We are going to receive this data coming down the
pipeline on this new thread now.” See section on receive(on:).
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [unowned self] catFact in
Scheduler
dataToView = catFact
You need to specify a “Scheduler”. A scheduler specifies how and where
work will take place. I’m specifying I want work done on the main thread.
(Run loops manage events and work. It allows multiple things to happen
simultaneously.)
})
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
137
Combine Mastery in SwiftUI
Publishers
DataTaskPublisher - Sink
struct CatFact: Decodable {
let _id: String
Sink
let text: String
}
There are two sink subscribers:
1. sink(receiveValue:)
2. sink(receiveCompletion:receiveValue:)
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
When it comes to this pipeline, we are forced to use the second one
because this pipeline can fail. Meaning the publisher and other operators
can throw an error.
@Published var dataToView: [CatFact] = []
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
In this pipeline, the dataTaskPublisher can throw an error and the
decode operator can throw an error.
URLSession.shared.dataTaskPublisher(for: url)
Xcode’s autocomplete won’t even show you the first sink option for this
pipeline so you don’t have to worry about which one to pick.
.map { $0.data }
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
Handling Errors
.sink(receiveCompletion: { completion in
There are many different ways you can handle errors that might be
thrown using operators or subscribers. For more information on options,
look at the chapter Handling Errors.
print(completion)
}, receiveValue: { [unowned self] catFact in
dataToView = catFact
I’m not going to cover all of them here. Instead, I’ll just show you a way to
inspect the error and display a generic message in an alert on the view
using another example on the next page.
})
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
138
Combine Mastery in SwiftUI
Publishers
Handling Errors - View
struct DataTaskPublisher_Errors: View {
@StateObject private var vm = DataTaskPublisher_ErrorsViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("DataTaskPublisher",
subtitle: "Handling Errors",
desc: "Here is an example of displaying an alert with an error message if
an error is thrown in the pipeline.")
List(vm.dataToView, id: \._id) { catFact in
Text(catFact.text)
}
Use width: 214
.font(.title3)
}
.font(.title)
.onAppear {
vm.fetch()
}
.alert(item: $vm.errorForAlert) { errorForAlert in
Alert(title: Text(errorForAlert.title),
One way the alert modifier
works is it can monitor a
@Published property. If
that property becomes not
nil then it will pass the value
of that property into a
closure and we use that
value to create and present
our Alert.
message: Text(errorForAlert.message))
Let’s look at the view model
to see how we are setting
that errorForAlert
property.
}
}
}
www.bigmountainstudio.com
139
Combine Mastery in SwiftUI
Publishers
Handling Errors - View Model
struct ErrorForAlert: Error, Identifiable {
Notice the ErrorForAlert conforms to Identifiable. This just means
you need to give it a property called “id” to conform to it.
let id = UUID()
let title = "Error"
var message = "Please try again later."
This is needed for the alert modifier on the view. It can only monitor
types that conform to Identifiable.
}
class DataTaskPublisher_ErrorsViewModel: ObservableObject {
@Published var dataToView: [CatFact] = []
@Published var errorForAlert: ErrorForAlert?
View
var cancellables: Set<AnyCancellable> = []
As soon as errorForAlert is not nil, this alert modifier will show an
Alert on the UI with the title and message from the ErrorForAlert:
func fetch() {
.alert(item: $vm.errorForAlert) { errorForAlert in
// See next page
Alert(title: Text(errorForAlert.title),
...
message: Text(errorForAlert.message))
}
}
}
www.bigmountainstudio.com
140
Combine Mastery in SwiftUI
Publishers
class DataTaskPublisher_ErrorsViewModel: ObservableObject {
?
@Published var dataToView: [CatFact] = []
@Published var errorForAlert: ErrorForAlert?
var cancellables: Set<AnyCancellable> = []
I changed the URL so that
we don’t get back the
expected JSON.
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/nothing")!
URLSession.shared.dataTaskPublisher(for: url)
You may notice this code looks a little
different from your traditional switch
case control flow.
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
if case .failure(let error) = completion {
errorForAlert = ErrorForAlert(message: "Details: \(error.localizedDescription)")
This is a shorthand to examine just one
case of an enum that has an
associated value like failure. This is
because we’re only interested when
the completion is a failure.
Learn more about if case here.
}
}, receiveValue: { [unowned self] catFact in
dataToView = catFact
})
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
If the pipeline completes because of an error, then the
errorForAlert property is populated with a new ErrorForAlert.
This will trigger an Alert to be presented on the view.
141
Combine Mastery in SwiftUI
Publishers
Error Options
You learned how to look for an error in the sink subscriber and show an Alert on the UI. Your options here can be expanded.
The dataTaskPublisher returns a URLResponse (as you can see in the map operator input parameter). You can also inspect this response and depending on the
code, you can notify the user as to why it didn’t work or take some other action. In this case, an exception is not thrown. But you might want to throw an exception
because when the data gets to the decode operator, it could throw an error because the decoding will most likely fail.
Codes Type
Description
1xx
Informational
responses
The server is thinking through the error.
2xx
Success
The request was successfully completed and the server gave the
browser the expected response.
3xx
Redirection
You got redirected somewhere else. The request was received,
but there’s a redirect of some kind.
4xx
Client errors
5xx
Server errors
Throw Errors
Page not found. The site or page couldn’t be reached. (The
request was made, but the page isn’t valid — this is an error on
the website’s side of the conversation and often appears when
a page doesn’t exist on the site.)
Failure. A valid request was made by the client but the server
failed to complete the request.
When it comes to throwing errors from operators, you
want to look for operators that start with the word “try”.
This is a good indication that the operator will allow you to
throw an error and so skip all the other operators
between it and your subscriber.
For example, if you wanted to throw an error from the
map operator, then use the tryMap operator instead.
Hide Errors
You may not want to show any error at all to the user and
instead hide it and take some other action in response.
For example, you could use the replaceError operator to
catch the error and then publish some default value
instead.
Source: https://moz.com/learn/seo/http-status-codes
www.bigmountainstudio.com
142
Combine Mastery in SwiftUI
DataTaskPublisher for Images
error
https://...
(
,
)
This section will show you an example of how to use the DataTaskPublisher to get an image using a URL.
Publishers
Getting an Image - View
struct DataTaskPublisher_ForImages: View {
@StateObject private var vm = DataTaskPublisher_ForImagesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("DataTaskPublisher",
subtitle: "For Images",
desc: "You can use the dataTaskPublisher operator to download images with
a URL.")
vm.imageView
}
Use width: 214
.font(.title)
.onAppear {
vm.fetch()
}
.alert(item: $vm.errorForAlert) { errorForAlert in
Alert(title: Text(errorForAlert.title),
message: Text(errorForAlert.message))
}
}
}
www.bigmountainstudio.com
In this example, the Big Mountain
Studio logo is being downloaded
using a URL.
If there’s an error, the alert modifier
will show an Alert with a message to
the user.
144
Combine Mastery in SwiftUI
Publishers
Getting an Image - View Model
class DataTaskPublisher_ForImagesViewModel: ObservableObject {
@Published var imageView: Image?
@Published var errorForAlert: ErrorForAlert?
var cancellables: Set<AnyCancellable> = []
Note: To understand all of these
parts better, I recommend looking at
the previous section of the
DataTaskPublisher.
func fetch() {
let url = URL(string: “https://d31ezp3r8jwmks.cloudfront.net/C3JrpZx1ggNrDXVtxNNcTz3t")!
URLSession.shared.dataTaskPublisher(for: url)
The tryMap operator is like map
.map { $0.data }
except it allows you to throw an error.
.tryMap { data in
guard let uiImage = UIImage(data: data) else {
throw ErrorForAlert(message: "Did not receive a valid image.")
}
return Image(uiImage: uiImage)
}
If the data received cannot
.receive(on: RunLoop.main)
be made into a UIImage
.sink(receiveCompletion: { [unowned self] completion in
then an error will be thrown
if case .failure(let error) = completion {
and the user will see it.
if error is ErrorForAlert {
errorForAlert = (error as! ErrorForAlert)
} else {
errorForAlert = ErrorForAlert(message: "Details: \
(error.localizedDescription)")
}
}
}, receiveValue: { [unowned self] image in
The sink’s completion closure is
imageView = image
looking for two different types of
})
.store(in: &cancellables)
errors. The first one is checking if
}
it’s the error thrown in the tryMap.
}
www.bigmountainstudio.com
145
Use width: 214
Combine Mastery in SwiftUI
Publishers
Getting an Image with ReplaceError - View
struct DataTaskPublisher_ReplaceError: View {
@StateObject private var vm = DataTaskPublisher_ReplaceErrorViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("DataTaskPublisher",
subtitle: "ReplaceError",
desc: "If any errors occur in the pipeline, you can use the replaceError
operator to supply default data.")
vm.imageView
Use width: 214
}
.font(.title)
.onAppear {
vm.fetch()
}
}
This view is mostly the same as the
previous example.
}
But in this case, if there is any kind of
error you will see a default image
presented instead of an alert.
www.bigmountainstudio.com
146
Combine Mastery in SwiftUI
Publishers
Getting an Image with ReplaceError - View Model
class DataTaskPublisher_ReplaceErrorViewModel: ObservableObject {
@Published var imageView: Image?
There is no image at this URL
so trying to convert the data to
a UIImage will fail.
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://www.bigmountainstudio.com/image1")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.tryMap { data in
guard let uiImage = UIImage(data: data) else {
throw ErrorForAlert(message: "Did not receive a valid image.")
}
return Image(uiImage: uiImage)
}
.replaceError(with: Image("blank.image"))
.receive(on: RunLoop.main)
If an error comes down the pipeline the
replaceError operator will receive it
and republish the blank image instead.
.sink { [unowned self] image in
imageView = image
}
.store(in: &cancellables)
The pipeline now knows that no error/failure will be sent downstream after the replaceError operator.
}
}
www.bigmountainstudio.com
Xcode autocomplete will now let you use the sink(receiveValue:) whereas before it would not.
Before you could ONLY use the sink(receiveCompletion:receiveValue:) operator because it
detected a failure could be sent downstream. Learn more in the Handling Errors chapter.
147
Combine Mastery in SwiftUI
OPERATORS
Operators
Organization
For this part of the book I organized the operators into groups using the same group names that Apple uses to organize their operators.
Applying Matching Criteria
to Elements
•
•
•
•
•
•
•
• AllSatisfy
• TryAllSatisfy
• Contains
• Contains(where:)
• TryContains(where:)
Filtering Elements
•
•
•
•
•
•
•
•
CompactMap
Applying Mathematical
Applying Sequence
Operations on Elements
Operations to Elements
Count
• Append
Max
Max(by:)
TryMax(by:)
TryMin(by:)
• Prepend
Reducing Elements
• Collect By Count
TryFilter
• ReplaceNil
RemoveDuplicates
• SetFailureType
• Scan
ReplaceEmpty
• TryScan
www.bigmountainstudio.com
Selecting Specific
Elements
• TryMap
TryRemoveDuplicates
• MeasureInterval
• Prefix(untilOutputFrom:)
Filter
RemoveDuplicates(by:)
• DropFirst
Min(by:)
• Collect
TryCompactMap
• Delay(for:)
• Prefix
• Map
• Collect By Time
• Collect By Time or Count
• IgnoreOutput
• Reduce
• TryReduce
149
• Debounce
• Drop(untilOutputFrom:)
Min
Mapping Elements
Controlling Timing
• First
• First(where:)
• TryFirst(where:)
• Last
• Last(where:)
• TryLast(where:)
• Output(at:)
• Output(in:)
• Throttle
• Timeout
Specifying
Schedulers
• Overview
• Receive(on:)
• Subscribe(on:)
Combine Mastery in SwiftUI
APPLYING MATCHING
CRITERIA TO ELEMENTS
?
?
true
These operators will evaluate items coming through a pipeline and match them against the criteria you specify and publish the results in different ways.
AllSatisfy
==
true
Use the allSatisfy operator to test all items coming through the pipeline meet your specified criteria. As soon as one item does NOT meet your criteria, a false is
published and the pipeline is finished/closed. Otherwise, if all items met your criteria then a true is published.
􀎷
Operators
allSatisfy - View
struct AllSatisfy_Intro: View {
@State private var number = ""
@State private var resultVisible = false
@StateObject private var vm = AllSatisfy_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView(“AllSatisfy", subtitle: "Introduction",
desc: "Use allSatisfy operator to test all items against a condition. If
all items satisfy your criteria, a true is returned, else a false is returned.")
.layoutPriority(1)
HStack {
TextField("add a number", text: $number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Button(action: {
vm.add(number: number)
number = ""
}, label: { Image(systemName: “plus") })
}.padding()
List(vm.numbers, id: \.self) { number in
Text("\(number)")
}
Spacer(minLength: 0)
Button("Fibonacci Numbers?") {
vm.allFibonacciCheck()
resultVisible = true
}
Text(vm.allFibonacciNumbers ? "Yes" : "No")
.opacity(resultVisible ? 1 : 0)
}
.padding(.bottom)
.font(.title)
}
}
www.bigmountainstudio.com
152
The allFibonacciCheck will see if all
numbers entered are in the Fibonacci
sequence.
(A Fibonacci number is one that is the
result of adding the previous two
numbers, starting with 0 and 1.
Example: 0,1,1,2,3,5,8,…)
Combine Mastery in SwiftUI
Operators
allSatisfy - View Model
class AllSatisfy_IntroViewModel: ObservableObject {
The allSatisfy operator will
check each number in the numbers
array to see if they are in the Fibonacci
sequence.
@Published var numbers: [Int] = []
@Published var allFibonacciNumbers = false
func allFibonacciCheck() {
let fibonacciNumbersTo144 = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
numbers.publisher
.allSatisfy { (number) in
If all are Fibonacci numbers, then true
is assigned to allFibonacciNumbers
property and the pipeline finishes
normally.
But as soon as allSatisfy finds a
number that is not a Fibonacci
number, then a false is published and
the pipeline finishes early.
fibonacciNumbersTo144.contains(number)
}
.assign(to: &$allFibonacciNumbers)
}
func add(number: String) {
if number.isEmpty { return }
numbers.append(Int(number) ?? 0)
}
}
www.bigmountainstudio.com
Note: You may also notice that I’m using
numbers.publisher here instead of $numbers.
In this situation, $numbers will not work because its
type is an array, not an individual item in the array.
By using numbers.publisher, I’m actually using the
Sequence publisher so each item in the array will go
through the pipeline individually.
153
Shorthand Argument Names
Here is an alternative way to write this using
shorthand argument names:
.allSatisfy {
fibonacciNumbersTo144.contains($0)
}
Combine Mastery in SwiftUI
TryAllSatisfy
error
if
throw
or
true
The tryAllSatisfy operator works just like allSatisfy except it can also publish an error.
So if all items coming through the pipeline satisfy the criteria you specify, then a true will be published. But as soon as the first item fails to satisfy the criteria, a false
is published and the pipeline is finished, even if there are still more items in the pipeline.
Ultimately, the subscriber will receive a true, false, or error and finish.
􀎷
Operators
TryAllSatisfy - View
struct TryAllSatisfy_Intro: View {
@State private var number = ""
@State private var resultVisible = false
@StateObject private var vm = TryAllSatisfy_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("AllSatisfy",
subtitle: "Introduction",
desc: "The tryAllSatisfy operator works like allSatisfy except now the
subscriber can also receive an error in addition to a true or
false.")
.layoutPriority(1)
HStack {
TextField("add a number < 145", text: $number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Button(action: {
vm.add(number: number)
number = ""
}, label: { Image(systemName: "plus") })
}
The view is continued on the next
page.
.padding()
www.bigmountainstudio.com
The idea here is that when the
pipeline will return true if all
numbers are Fibonacci numbers but
if any number is over 144, an error is
thrown and displayed as an alert.
155
Combine Mastery in SwiftUI
Operators
List(vm.numbers, id: \.self) { number in
Text("\(number)")
}
Spacer(minLength: 0)
Button("Fibonacci Numbers?") {
vm.allFibonacciCheck()
resultVisible = true
}
Text(vm.allFibonacciNumbers ? "Yes" : "No")
.opacity(resultVisible ? 1 : 0)
}
.padding(.bottom)
.font(.title)
.alert(item: $vm.invalidNumberError) { error in
Alert(title: Text("A number is greater than 144"),
primaryButton: .default(Text("Start Over"), action: {
vm.numbers.removeAll()
}),
secondaryButton: .cancel()
)
}
Use width: 214
}
}
When invalidNumberError has a value, this alert will show.
This error will get set when a number above 144 is detected.
www.bigmountainstudio.com
156
Combine Mastery in SwiftUI
Operators
TryAllSatisfy - View Model
class TryAllSatisfy_IntroViewModel: ObservableObject {
@Published var numbers: [Int] = []
@Published var allFibonacciNumbers = false
@Published var invalidNumberError: InvalidNumberError?
struct InvalidNumberError: Error, Identifiable
{
var id = UUID()
}
func allFibonacciCheck() {
let fibonacciNumbersTo144 = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
_ = numbers.publisher
.tryAllSatisfy { (number) in
if number > 144 { throw InvalidNumberError() }
return fibonacciNumbersTo144.contains(number)
}
.sink { [unowned self] (completion) in
switch completion {
case .failure(let error):
self.invalidNumberError = error as? InvalidNumberError
default:
break
}
} receiveValue: { [unowned self] (result) in
allFibonacciNumbers = result
}
This is the custom Error object that will be thrown. It
also conforms to Identifiable so it can be used to
show an alert in the view.
If tryAllSatisfy detects a number over 144, an error
is thrown and the pipeline will then finished
(completed).
The subscriber (sink) receives the error in the
receivesCompletion closure.
}
func add(number: String) {
if number.isEmpty { return }
numbers.append(Int(number) ?? 0)
}
}
www.bigmountainstudio.com
157
Combine Mastery in SwiftUI
Contains
==
true
The contains operator has just one purpose - to let you know if an item coming through your pipeline matches the criteria you specify. It will publish a true when a
match is found and then finishes the pipeline, meaning it stops the flow of any remaining data.
If no values match the criteria then a false is published and the pipeline finishes/closes.
Operators
Contains - View
struct Contains_Intro: View {
@StateObject private var vm = Contains_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Contains",
subtitle: "Introduction",
desc: "The contains operator will publish a true and finish the pipeline
when an item coming through matches its criteria.")
Text("House Details")
.fontWeight(.bold)
Group {
Use width: 214
Text(vm.description)
Toggle("Basement", isOn: $vm.basement)
Toggle("Air Conditioning", isOn: $vm.airconditioning)
Toggle("Heating", isOn: $vm.heating)
}
.padding(.horizontal)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
159
Combine Mastery in SwiftUI
Operators
Contains - View Model
class Contains_IntroViewModel: ObservableObject {
@Published var description = ""
@Published var airconditioning = false
@Published var heating = false
@Published var basement = false
private var cancellables: [AnyCancellable] = []
func fetch() {
let incomingData = ["3 bedrooms", "2 bathrooms", "Air conditioning", "Basement"]
incomingData.publisher
.prefix(2)
.sink { [unowned self] (item) in
description += item + "\n"
}
.store(in: &cancellables)
The prefix operator just returns
the first 2 items in this pipeline.
These single-purpose publishers will just look for one match and
publish a true or false to the @Published properties.
incomingData.publisher
.contains("Air conditioning")
.assign(to: &$airconditioning)
Remember, when the first match is found, the publisher will
finish, even if there are more items in the pipeline.
incomingData.publisher
.contains("Heating")
.assign(to: &$heating)
incomingData.publisher
.contains("Basement")
.assign(to: &$basement)
Can I use contains on my custom data objects?
If they conform the Equatable protocol you can. The
Equatable protocol requires that you specify what determines if
two of your custom data objects are equal. You may also want to
look at the contains(where: ) operator on the next page.
}
}
www.bigmountainstudio.com
160
Combine Mastery in SwiftUI
Contains(where: )
12
12
12
1==
2==
true
This contains(where:) operator gives you a closure to specify your criteria to find a match. This could be useful where the items coming through the pipeline are
not simple primitive types like a String or Int. Items that do not match the criteria are dropped (not published) and when the first item is a match, the boolean true is
published.
When the first match is found, the pipeline is finished/stopped.
If no matches are found at the end of all the items, a boolean false is published and the pipeline is finished/stopped.
Operators
Contains(where: ) - View
struct Contains_Where: View {
@StateObject private var vm = Contains_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Contains",
subtitle: "Where",
desc: "The contains(where:) operator will publish a true and finish the
pipeline when an item coming through matches the criteria you
specify within the closure it provides.")
Group {
Text(vm.fruitName)
Use width: 214
Toggle("Vitamin A", isOn: $vm.vitaminA)
Toggle("Vitamin B", isOn: $vm.vitaminB)
Toggle("Vitamin C", isOn: $vm.vitaminC)
}
.padding(.horizontal)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
162
Combine Mastery in SwiftUI
Operators
Contains(where: ) - View Model
class Contains_WhereViewModel: ObservableObject {
@Published var fruitName = ""
@Published var vitaminA = false
@Published var vitaminB = false
@Published var vitaminC = false
struct Fruit: Identifiable {
let id = UUID()
var name = ""
var nutritionalInformation = ""
}
func fetch() {
let incomingData = [Fruit(name: "Apples", nutritionalInformation: "Vitamin A, Vitamin C")]
_ = incomingData.publisher
.sink { [unowned self] (fruit) in
fruitName = fruit.name
}
Notice in this case I’m not storing the cancellable in a
property because I don’t need to. After the pipeline
finishes, I don’t have to hold on to a reference of it.
incomingData.publisher
.contains(where: { (fruit) -> Bool in
fruit.nutritionalInformation.contains("Vitamin A")
})
.assign(to: &$vitaminA)
These single-purpose publishers will just look for one
match and publish a true or false to the @Published
properties.
incomingData.publisher
.contains(where: { (fruit) -> Bool in
fruit.nutritionalInformation.contains("Vitamin B")
})
.assign(to: &$vitaminB)
Remember, when the first match is found, the publisher
will finish, even if there are more items in the pipeline.
incomingData.publisher
.contains { (fruit) -> Bool in
fruit.nutritionalInformation.contains("Vitamin C")
}
.assign(to: &$vitaminC)
Notice how this contains(where: ) is written differently
without the parentheses. This is another way to write the
operator that the compiler will still understand.
}
}
www.bigmountainstudio.com
163
Combine Mastery in SwiftUI
TryContains(where: )
error
12
12
12
2==
throw
2==
true
You have the option to look for items in your pipeline and publish a true for the criteria you specify or publish an error for the condition you set.
When an item matching your condition is found, a true will then be published and the pipeline will be finished/closed.
Alternatively, you can throw an error that will pass the error downstream and complete the pipeline with a failure. The subscriber will ultimately receive a true,
false, or error and finish.
􀎷
Operators
TryContains(where: ) - View
struct TryContains_Where: View {
@StateObject private var vm = TryContains_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryContains",
subtitle: "Introduction",
desc: "The tryContains(where: ) operator works like contains(where: )
except now the subscriber can also receive an error in addition to
a true or false.")
Text("Look for Salt Water in:")
Picker("Place", selection: $vm.place) {
Text("Nevada").tag("Nevada")
Text("Utah").tag("Utah")
Text("Mars").tag("Mars")
The picker is bound to place. So when the
}
user does a search, that place value is
.pickerStyle(SegmentedPickerStyle())
Button("Search") {
vm.search()
}
compared to all the items in the search
result to see if it exists or not.
Text("Result: \(vm.result)")
}
.font(.title)
.alert(item: $vm.invalidSelectionError) { alertData in
Alert(title: Text("Invalid Selection"))
}
}
If tryContains(where:)
throws an error, then this
alert will show.
See how on the next page.
}
www.bigmountainstudio.com
165
Combine Mastery in SwiftUI
Operators
TryContains(where: ) - View Model
struct InvalidSelectionError: Error, Identifiable {
var id = UUID()
}
This is the custom Error object that will be thrown.
It also conforms to Identifiable so it can be
used to show an alert in the view.
class TryContains_WhereViewModel: ObservableObject {
@Published var place = "Nevada"
@Published var result = ""
@Published var invalidSelectionError: InvalidSelectionError?
func search() {
let incomingData = ["Places with Salt Water", "Utah", "California"]
_ = incomingData.publisher
.dropFirst()
.tryContains(where: { [unowned self] (item) -> Bool in
if place == "Mars" {
throw InvalidSelectionError()
}
return item == place
})
.sink { [unowned self] (completion) in
switch completion {
case .failure(let error):
self.invalidSelectionError = error as? InvalidSelectionError
default:
break
}
} receiveValue: { [unowned self] (result) in
self.result = result ? "Found" : "Not Found"
}
}
}
www.bigmountainstudio.com
166
If the user selected Mars then an error is thrown.
The condition for when the error is thrown can be
anything you want.
But if an item from your data source contains the
place selected, then a true will be published and
the pipeline will finish.
Learn More
• dropFirst
Combine Mastery in SwiftUI
APPLYING MATHEMATICAL
OPERATIONS ON ELEMENTS
If you’re familiar with array functions to get count, min, and max values then these operators will be very easy to understand for you. If you are familiar with doing
queries in databases then you might recognize these operators as aggregate functions. (“Aggregate” just means to group things together to get one thing.)
Count
05
5
The count operator simply publishes the count of items it receives. It’s important to note that the count will not be published until the upstream publisher has
finished publishing all items.
Operators
Count - View
struct Count_Intro: View {
@StateObject private var vm = Count_IntroViewModel()
var body: some View {
NavigationView {
VStack(spacing: 20) {
HeaderView("", subtitle: "Introduction",
desc: "The count operator simply publishes the total number of items
it receives from the upstream publisher.")
Form {
NavigationLink(
destination: CountDetailView(data: vm.data),
label: {
Text(vm.title)
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(vm.count)")
})
}
}
.font(.title)
.navigationTitle("Count")
.onAppear { vm.fetch() }
}
}
Use width: 214
}
struct CountDetailView: View {
var data: [String]
var body: some View {
List(data, id: \.self) { datum in
Text(datum)
}
.font(.title)
}
}
www.bigmountainstudio.com
169
Combine Mastery in SwiftUI
Operators
Count - View Model
class Count_IntroViewModel: ObservableObject {
@Published var title = ""
@Published var data: [String] = []
@Published var count = 0
func fetch() {
title = "Major Rivers"
let dataIn = ["Mississippi", "Nile", "Yangtze", "Danube", "Ganges", "Amazon", "Volga",
"Rhine"]
data = dataIn
dataIn.publisher
.count()
.assign(to: &$count)
This is a very simplistic example of a
very simple operator.
Use width: 214
}
}
www.bigmountainstudio.com
170
Combine Mastery in SwiftUI
Max
The max operator will republish just the maximum value that it received from the upstream publisher. If the max operator receives 10 items, it’ll find the maximum
item and publish just that one item. If you were to sort your items in descending order then max would take the item at the top.
It’s important to note that the max operator publishes the maximum item ONLY when the upstream publisher has finished with all of its items.
Operators
Max - View
struct Max_Intro: View {
@StateObject private var vm = Max_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Max",
subtitle: "Introduction",
desc: "The max operator will publish the maximum value once the upstream
publisher is finished.")
.layoutPriority(1)
List {
Section(footer: Text("Max: \(vm.maxValue)").bold()) {
ForEach(vm.data, id: \.self) { datum in
Text(datum)
}
}
}
Use width: 214
List {
Section(footer: Text("Max: \(vm.maxNumber)").bold()) {
ForEach(vm.numbers, id: \.self) { number in
Text("\(number)")
}
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
This view shows a collection of data and the
minimum values for strings and ints using the
max operator.
}
}
www.bigmountainstudio.com
172
Combine Mastery in SwiftUI
Operators
Max - View Model
class Max_IntroViewModel: ObservableObject {
@Published var data: [String] = []
@Published var maxValue = ""
@Published var numbers: [Int] = []
@Published var maxNumber = 0
func fetch() {
let dataIn = ["Aardvark", "Zebra", "Elephant"]
data = dataIn
dataIn.publisher
.max()
.assign(to: &$maxValue)
Pretty simple operator. It will get the
max string or max int.
let dataInNumbers = [900, 245, 783]
numbers = dataInNumbers
dataInNumbers.publisher
.max()
.assign(to: &$maxNumber)
}
}
The maximum value is ONLY
published once the publisher has
sent all of the items through the
pipeline.
Finding the max value depends
on types conforming to the
Comparable protocol.
The Comparable protocol allows
the Swift compiler to know how to
order objects and which is greater
or lesser than others.
But what if a type does not
conform to the Comparable
protocol? How can you find the
max value?
Then you can use the max(by:)
operator. See next page.
www.bigmountainstudio.com
173
Combine Mastery in SwiftUI
Max(by:)
The max(by:) operator will republish just the maximum value it received from the upstream publisher using the criteria you specify within a closure. Inside the
closure, you will get the current and next item. You can then weigh them against each other specify which one comes before the other. Now that the pipeline knows
how to sort them, it can republish the minimum item.
It’s important to note that the max(by:) operator publishes the max item ONLY when the upstream publisher has finished with all of its items.
Operators
Max(by:) - View
struct MaxBy_Intro: View {
@StateObject private var vm = MaxBy_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Max(by: )",
subtitle: "Introduction",
desc: "The max(by: ) operator provides a closure so you can specify your
own logic to determine which item is the max.")
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.city)
Use width: 214
.foregroundColor(.secondary)
}
Text("Max City: \(vm.maxValue)")
.bold()
}
In this view, each row is a Profile struct
with a name and city.
And I’m getting the maximum city (as a
string).
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
175
Combine Mastery in SwiftUI
Operators
Max(by:) - View Model
struct Profile: Identifiable {
let id = UUID()
var name = ""
var city = ""
}
class MaxBy_IntroViewModel: ObservableObject {
@Published var profiles: [Profile] = []
@Published var maxValue = ""
func fetch() {
let dataIn = [Profile(name: "Igor", city: "Moscow"),
The max(by:) operator receives the current and next item
in the pipeline.
You can then define your criteria to get the max value.
Profile(name: "Rebecca", city: "Atlanta"),
Profile(name: "Christina", city: "Stuttgart"),
Profile(name: "Lorenzo", city: "Rome"),
Profile(name: "Oliver", city: "London")]
I should rephrase that. You’re not exactly specifying the
criteria to get the max value, instead, you’re specifying the
ORDER so that whichever item is last is the maximum.
profiles = dataIn
_ = dataIn.publisher
.max(by: { (currentItem, nextItem) -> Bool in
return currentItem.city < nextItem.city
})
Shorthand Argument Names
.sink { [unowned self] profile in
Note: An even shorter way to write this is to use shorthand
argument names like this:
maxValue = profile.city
}
}
.max { $0.city < $1.city }
}
www.bigmountainstudio.com
176
Combine Mastery in SwiftUI
TryMax(by:)
error
if
throw
When you want to return the maximum item or the possibility of an error too, then you would use the tryMax(by:) operator. It works just like the max(by:)
operator but can also throw an error.
Operators
TryMax(by:) - View
struct TryMax_Intro: View {
@StateObject private var vm = TryMax_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("tryMax(by: )",
subtitle: "Introduction",
desc: "The tryMax(by: ) operator provides a closure so you can specify
your own logic to determine which item is the maximum or throw an error.")
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.country)
.foregroundColor(.secondary)
If tryMax(by:) throws an
}
error, then this alert will show.
Use width: 214
Text("Max Country: \(vm.maxValue)")
.bold()
See how on the next page.
}
.font(.title)
.alert(item: $vm.invalidCountryError) { alertData in
Alert(title: Text("Invalid Country:"), message: Text(alertData.country))
}
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
178
Combine Mastery in SwiftUI
Operators
TryMax(by:) - View Model
struct UserProfile: Identifiable {
let id = UUID()
var name = ""
var city = ""
var country = ""
}
class TryMax_IntroViewModel: ObservableObject {
@Published var profiles: [UserProfile] = []
@Published var maxValue = ""
@Published var invalidCountryError: InvalidCountryError?
func fetch() {
let dataIn = [UserProfile(name:
UserProfile(name:
UserProfile(name:
UserProfile(name:
"Igor", city: "Moscow", country: "Russia"),
"Rebecca", city: "Atlanta", country: "United States"),
"Christina", city: "Stuttgart", country: "Germany"),
"Lorenzo", city: "Rome", country: "Italy")]
profiles = dataIn
_ = dataIn.publisher
.tryMax(by: { (current, next) -> Bool in
if current.country == "United States" {
throw InvalidCountryError(country: "United States")
}
return current.country < next.country
})
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.invalidCountryError = error as? InvalidCountryError
}
} receiveValue: { [unowned self] (userProfile) in
self.maxValue = userProfile.country
}
}
You may notice this code looks a little different from your
traditional switch case control flow.
This is a shorthand to examine just one case of an enum
that has an associate value like failure. This is because
we’re only interested when the completion is a failure.
You can learn more about if case here.
}
www.bigmountainstudio.com
struct InvalidCountryError: Error, Identifiable {
var id = UUID()
var country = ""
}
179
Combine Mastery in SwiftUI
Min
The min operator will republish just the minimum value that it received from the upstream publisher. If the min operator receives 10 items, it’ll find the minimum
item and publish just that one item. If you were to sort your items in ascending order then min would take the item at the top.
It’s important to note that the min operator publishes the minimum item ONLY when the upstream publisher has finished with all of its items.
Operators
Min - View
struct Min_Intro: View {
@StateObject private var vm = Min_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Min",
subtitle: "Introduction",
desc: "The min operator will publish the minimum value once the upstream
publisher is finished.")
.layoutPriority(1)
List {
Section(footer: Text("Min: \(vm.minValue)").bold()) {
ForEach(vm.data, id: \.self) { datum in
Text(datum)
}
}
}
Use width: 214
List {
Section(footer: Text("Min: \(vm.minNumber)").bold()) {
ForEach(vm.numbers, id: \.self) { number in
Text("\(number)")
}
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
This view shows a collection of data and the
minimum values for strings and ints using the
min operator.
}
}
www.bigmountainstudio.com
181
Combine Mastery in SwiftUI
Operators
Min - View Model
class Min_IntroViewModel: ObservableObject {
@Published var data: [String] = []
@Published var minValue = ""
@Published var numbers: [Int] = []
@Published var minNumber = 0
func fetch() {
let dataIn = ["Aardvark", "Zebra", "Elephant"]
data = dataIn
dataIn.publisher
.min()
.assign(to: &$minValue)
Pretty simple operator. It will get the
minimum string or minimum int.
Finding the minimum value depends on types
conforming to the Comparable protocol.
let dataInNumbers = [900, 245, 783]
numbers = dataInNumbers
dataInNumbers.publisher
.min()
.assign(to: &$minNumber)
}
}
The minimum value is ONLY
published once the publisher has
sent all of the items through the
pipeline.
The Comparable protocol allows the Swift
compiler to know how to order objects and
which is greater or lesser than others.
But what if a type does not conform to the
Comparable protocol? How can you find the
min value?
Then you can use the min(by:) operator.
See next page.
www.bigmountainstudio.com
182
Combine Mastery in SwiftUI
Min(by:)
The min(by:) operator will republish just the minimum value it received from the upstream publisher using the criteria you specify within a closure. Inside the
closure, you will get the current and next item. You can then weigh them against each other specify which one comes before the other. Now that the pipeline knows
how to sort them, it can republish the minimum item.
It’s important to note that the min(by:) operator publishes the min item ONLY when the upstream publisher has finished with all of its items.
Operators
Min(by:) - View
struct MinBy_Intro: View {
@StateObject private var vm = MinBy_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Min(by: )",
subtitle: "Introduction",
desc: "The min(by: ) operator provides a closure so you can specify your
own logic to determine which item is the minimum.")
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Use width: 214
Text(profile.city)
.foregroundColor(.secondary)
}
Text("Min City: \(vm.minValue)")
.bold()
}
In this view, each row is a Profile struct with
a name and city.
And I’m getting the minimum city (as a
string).
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
184
Combine Mastery in SwiftUI
Operators
Min(by:) - View Model
class MinBy_IntroViewModel: ObservableObject {
struct Profile: Identifiable {
@Published var profiles: [Profile] = []
let id = UUID()
@Published var minValue = ""
var name = ""
var city = ""
}
func fetch() {
let dataIn = [Profile(name: "Igor", city: "Moscow"),
Profile(name: "Rebecca", city: "Atlanta"),
The min(by:) operator receives the current and next item
in the pipeline.
You can then define your criteria to get the min value.
Profile(name: "Christina", city: "Stuttgart"),
Profile(name: "Lorenzo", city: "Rome"),
Profile(name: "Oliver", city: "London")]
Well, you’re not actually specifying the criteria to get the min
value, instead, you’re specifying the ORDER so that
whichever item is last is the minimum.
profiles = dataIn
_ = dataIn.publisher
You may have also noticed that the logic is exactly the same
as the max(by:) operator. It’s because your logic is to simply
define how these items should be ordered and that’s it.
.min(by: { (currentItem, nextItem) -> Bool in
return currentItem.city < nextItem.city
})
.sink { [unowned self] profile in
minValue = profile.city
Shorthand Argument Names
}
Note: An even shorter way to write this is to use shorthand
argument names like this:
}
}
.max { $0.city < $1.city }
www.bigmountainstudio.com
185
Combine Mastery in SwiftUI
TryMin(by:)
error
if
throw
When you want to return the minimum item or the possibility of an error too, then you would use the tryMin(by:) operator. It works just like the min(by:)
operator but can also throw an error.
Operators
TryMin(by:) - View
struct TryMin_Intro: View {
@StateObject private var vm = TryMin_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("tryMin(by:)",
subtitle: "Introduction",
desc: "The tryMin(by:) operator provides a closure so you can specify
your own logic to determine which item is the minimum or throw an
error.")
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.country)
.foregroundColor(.secondary)
}
Use width: 214
Text("Min Country: \(vm.maxValue)")
.bold()
}
.font(.title)
.alert(item: $vm.invalidCountryError) { alertData in
Alert(title: Text("Invalid Country:"), message: Text(alertData.country))
}
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
187
Combine Mastery in SwiftUI
Operators
TryMin(by:) - View Model
class TryMin_IntroViewModel: ObservableObject {
@Published var profiles: [UserProfile] = []
@Published var maxValue = ""
@Published var invalidCountryError: InvalidCountryError?
func fetch() {
let dataIn = [UserProfile(name:
UserProfile(name:
UserProfile(name:
UserProfile(name:
struct UserProfile: Identifiable {
let id = UUID()
var name = ""
var city = ""
var country = ""
}
"Igor", city: "Moscow", country: "Russia"),
"Rebecca", city: "Atlanta", country: "United States"),
"Christina", city: "Stuttgart", country: "Germany"),
"Lorenzo", city: "Rome", country: "Italy")]
profiles = dataIn
_ = dataIn.publisher
.tryMin(by: { (current, next) -> Bool in
if current.country == "United States" {
throw InvalidCountryError(country: "United States")
}
return current.country < next.country
})
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.invalidCountryError = error as? InvalidCountryError
}
} receiveValue: { [unowned self] (userProfile) in
self.maxValue = userProfile.country
}
}
}
www.bigmountainstudio.com
struct InvalidCountryError: Error, Identifiable {
var id = UUID()
var country = ""
}
You may notice this code looks a little different from your
traditional switch case control flow.
This is a shorthand to examine just one case of an enum
that has an associate value like failure. This is because
we’re only interested when the completion is a failure.
You can learn more about if case here.
188
Combine Mastery in SwiftUI
APPLYING SEQUENCE
OPERATIONS TO ELEMENTS
These operators affect the sequence of how items are delivered in your pipeline. Examples are being able to add items to the beginning of your first published items
or at the end or removing a certain amount of items that first come through.
Append
“Last Item”
“Last Item”
“Second Item”
“First Item”
The append operator will publish data after the publisher has sent out all of its items.
Note: The word “append” means to add or attach something to something else. In this case, the operator attaches an item to the end.
Operators
Append
class Append_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["Amsterdam", "Oslo", "* Helsinki", "Prague", "Budapest"]
cancellable = dataIn.publisher
.append("(* - May change)")
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
This item will be published last after
all other items finish.
}
}
struct Append_Intro: View {
@StateObject private var vm = Append_IntroViewModel()
Use width: 214
var body: some View {
VStack(spacing: 20) {
HeaderView("Append",
subtitle: "Introduction",
desc: "The append operator will add data after the publisher sends out
all of its data.")
Text("Cities to tour")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
191
Combine Mastery in SwiftUI
Operators
Append - Multiple
class Append_MultipleViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["$100", "$220", "$87", "$3,400", "$12"]
cancellable = dataIn.publisher
.append("Total: $3,819")
.append("(tap refresh to update)")
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
}
}
Use width: 214
Note: The items are appended
AFTER the publisher finishes.
If the publisher never finishes,
the items will never get
appended.
A Sequence publisher is being
used here which automatically
finishes when the last item is
published. So the append will
always work here.
struct Append_Multiple: View {
@StateObject private var vm = Append_MultipleViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Append",
subtitle: "Multiple",
desc: "You can have multiple append operators. The last append will be
the last published.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
.fontWeight(datum.contains("Total") ? .bold : .regular)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.font(.title)
.onAppear { vm.fetch() }
}
}
www.bigmountainstudio.com
192
Combine Mastery in SwiftUI
Operators
Append - Warning - View
struct Append_Warning: View {
@StateObject private var vm = Append_WarningViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Append",
subtitle: "Warning",
desc: "Append will only work if the pipeline finishes. The append example
you see in the view model will never publish.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
Use width: 214
.fontWeight(datum.contains("Total") ? .bold : .regular)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
If we change the view model and try to append the items to the @Published property, you
will never see those 2 appended values as you saw on the previous page.
Let’s take a closer look at the view model on the next page.
www.bigmountainstudio.com
193
Combine Mastery in SwiftUI
Operators
Append - Warning - View Model
class Append_WarningViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
Why didn’t the items get appended?
init() {
cancellable = $dataToView
.append(["Total:
$3,819"])
.append(["(tap refresh to update)"])
.sink { (completion) in
print(completion)
It’s because the pipeline never finished. You can see in the Xcode debug console
window that the completion never printed.
Just keep this in mind when using this operator. You want to use it on a pipeline that
actually finishes.
} receiveValue: { (data) in
print(data)
}
}
func fetch() {
?
dataToView = ["$100", "$220", "$87", "$3,400", "$12"]
}
}
www.bigmountainstudio.com
194
Combine Mastery in SwiftUI
Operators
Append Pipelines - View
struct Append_Pipelines: View {
@StateObject private var vm = Append_PipelinesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Append",
subtitle: "Pipelines",
desc: "Not only can you append values, you can also append whole
pipelines so you get the values from another pipeline added to the
end of the first pipeline.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
Use width: 214
.fontWeight(datum.contains("READ") ? .bold : .regular)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
The UNREAD and READ data comes from two different pipelines.
You can append the READ pipeline data to the UNREAD pipeline.
See how this is done in the view model on the next page…
www.bigmountainstudio.com
195
Combine Mastery in SwiftUI
Operators
Append Pipelines - View Model
class Append_PipelinesViewModel: ObservableObject {
@Published var dataToView: [String] = []
var emails: AnyCancellable?
func fetch() {
let unread = ["New from Meng", "What Shai Mishali says about Combine"]
.publisher
.prepend("UNREAD")
let read = ["Donny Wals Newsletter", "Dave Verwer Newsletter", "Paul Hudson Newsletter"]
Here are two sources of data.
Each pipeline has its own
property.
.publisher
.prepend("READ")
emails = unread
.append(read)
This is where the read pipeline
is being appended on the
unread pipeline.
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
}
}
www.bigmountainstudio.com
196
Combine Mastery in SwiftUI
Drop(untilOutputFrom:)
In Combine, when the term “drop” is used, it means to not publish or send the item down the pipeline. When an item is “dropped”, it will not reach the subscriber. So
with the drop(untilOutputFrom:) operator, the main pipeline will not publish its items until it receives an item from a second pipeline that signals “it’s ok to start
publishing now.”
In the image above, the pipeline with the red ball is the second pipeline. Once a value is sent through, it’ll allow items to flow through the main pipeline. It’s sort of
like a switch.
􀎷
Operators
Drop(untilOutputFrom:) - View
struct DropUntilOutputFrom_Intro: View {
@StateObject private var vm = DropUntilOutputFrom_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Drop(untilOutputFrom: )",
subtitle: "Introduction",
desc: "This operator will prevent items from being published until it
gets data from another publisher.")
Button("Open Pipeline") {
vm.startPipeline.send(true)
}
List(vm.data, id: \.self) { datum in
Text(datum)
}
Spacer(minLength: 0)
Button("Close Pipeline") {
vm.cancellables.removeAll()
}
The idea here is that you have a
publisher that may or may not be
sending out data. But it won’t reach
the subscriber (or ultimately, the UI)
unless a second publisher sends out
data too.
The second publisher is what opens
the flow of data on the first publisher.
This Button sends a value through
the second publisher.
}
.font(.title)
}
Note: I’m not actually “closing” a pipeline. I’m just removing it from memory
which will stop it from publishing data.
}
www.bigmountainstudio.com
198
Combine Mastery in SwiftUI
Operators
Drop(untilOutputFrom:) - View Model
class DropUntilOutputFrom_IntroViewModel: ObservableObject {
@Published var data: [String] = []
var startPipeline = PassthroughSubject<Bool, Never>()
var cancellables: [AnyCancellable] = []
let timeFormatter = DateFormatter()
init() {
timeFormatter.timeStyle = .medium
When the startPipeline receives a value it sends it straight through and the
Timer publisher detects it and that’s when the pipeline is fully connected and data
can freely flow through to the subscriber.
Timer.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.drop(untilOutputFrom: startPipeline)
.map { datum in
return self.timeFormatter.string(from: datum)
}
.sink{ [unowned self] (datum) in
data.append(datum)
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
Notes
• More values sent through the startPipeline have no effect on the Timer’s
•
pipeline.
In this example, I use a PassthroughSubject<Bool, Never> but you don’t
really have to send a value through to trigger the drop operator. I could have just
used PassthroughSubject<Void, Never> and on the UI, the button code
would be: vm.startPipeline.send()
199
Combine Mastery in SwiftUI
DropFirst
The dropFirst operator can prevent a certain number of items from initially being published.
􀎷
Operators
DropFirst - View
struct DropFirst_Intro: View {
@StateObject private var vm = DropFirst_IntroViewModel()
var statusColor: Color {
switch vm.isUserIdValid {
case .ok:
return Color.green
case .invalid:
return Color.red
default:
return Color.secondary
}
}
We want the border color around the
text field to default to gray
(secondary).
If the text is less than 8 characters, we
want to change the border color to
red. Over 8 characters will be green.
var body: some View {
VStack(spacing: 20) {
HeaderView("DropFirst",
subtitle: "Introduction",
desc: "The dropFirst operator will prevent the first item through the
pipeline from being published. This can be helpful with validation
pipelines. ")
Text("Create a User ID")
TextField("user id", text: $vm.userId)
.padding()
.border(statusColor)
.padding()
}
.font(.title)
}
}
www.bigmountainstudio.com
201
Combine Mastery in SwiftUI
Operators
DropFirst - View Model
enum Validation {
case ok
case invalid
case notEvaluated
}
class DropFirst_IntroViewModel: ObservableObject {
@Published var userId = ""
@Published var isUserIdValid = Validation.notEvaluated
When the view loads and its view model
is initialized, the pipeline will actually
run because an empty string is
assigned to userId.
This will change the status to invalid
and cause the border to be red before
the user has even done anything.
init() {
$userId
.dropFirst()
.map { userId -> Validation in
userId.count > 8 ? .ok : .invalid
}
The dropFirst will prevent this from
happening and the isUserIdValid
property will not change.
Use width: 214
.assign(to: &$isUserIdValid)
}
}
www.bigmountainstudio.com
202
Combine Mastery in SwiftUI
Operators
DropFirst(count: ) - View
class DropFirst_CountViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["New England:", "(6 States)", "Vermont", "New Hampshire", "Maine",
"Massachusetts", "Connecticut", "Rhode Island"]
cancellable = dataIn.publisher
.dropFirst(2)
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
}
}
Use width: 214
struct DropFirst_Count: View {
@StateObject private var vm = DropFirst_CountViewModel()
Pipeline: The idea here is that I
know the first two items in the data
I retrieved are always informational.
So I want to skip them using the
dropFirst operator.
var body: some View {
VStack(spacing: 20) {
HeaderView("DropFirst",
subtitle: "Count",
desc: "You can also specify how many items you want dropped before you
start allowing items through your pipeline.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
.onAppear { vm.fetch() }
}
}
www.bigmountainstudio.com
203
Combine Mastery in SwiftUI
Prefix
4
The prefix operator will republish items up to a certain count that you specify. So if a pipeline has 10 items but your prefix operator specifies 4, then only 4 items
will reach the subscriber.
The word “prefix” means to “put something in front of something else”. Here it means to publish items in front of the max number you specify. (Personally, I think this
operator should have been publish(first: Int). )
When the prefix number is hit, the pipeline finishes, meaning it will no longer publish anything else.
􀎷
Operators
Prefix - View
struct Prefix_Intro: View {
@StateObject private var vm = Prefix_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Prefix",
subtitle: "Introduction",
desc: "Use the prefix operator to get the first specified number of items
from a pipeline.")
Text("Limit Results")
Slider(value: $vm.itemCount, in: 1...10, step: 1)
Text(“\(Int(vm.itemCount))")
Button("Fetch Data") {
vm.fetch()
}
List(vm.data, id: \.self) { datum in
Text(datum)
}
Spacer(minLength: 0)
}
.font(.title)
}
}
www.bigmountainstudio.com
205
Combine Mastery in SwiftUI
Operators
Prefix - View Model
class Prefix_IntroViewModel: ObservableObject {
@Published var data: [String] = []
@Published var itemCount = 5.0
func fetch() {
data.removeAll()
let fetchedData = ["Result 1", "Result 2", "Result 3", "Result 4", "Result 5", "Result
6", "Result 7", "Result 8", "Result 9", "Result 10"]
_ = fetchedData.publisher
.prefix(Int(itemCount))
The prefix operator only republishes items up to the number you specify. It will then
finish (close/stop) the pipeline even if there are more items.
.sink { [unowned self] (result) in
data.append(result)
}
}
}
Notice in this case I’m not storing the cancellable into a
property because I don’t need to. After the pipeline
finishes, I don’t have to hold on to a reference of it.
www.bigmountainstudio.com
206
Combine Mastery in SwiftUI
Prefix(untilOutputFrom:)
The prefix(untilOutputFrom:) operator will let items continue to be passed through a pipeline until it receives a value from another pipeline. If you’re familiar with
the drop(untilOutputFrom:) operator, then this is the opposite of that. The second pipeline is like a switch that closes the first pipeline.
The word “prefix” means to “put something in front of something else”. Here it means to publish items in front of or before the output of another pipeline.
In the image above, the pipeline with the red ball is the second pipeline. When it sends a value through, it will cut off the flow of the main pipeline.
􀎷
Operators
Prefix(untilOutputFrom:) - View
struct PrefixUntilOutputFrom_Intro: View {
@StateObject private var vm = PrefixUntilOutputFrom_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Prefix(UntilOutputFrom: )",
subtitle: "Introduction",
desc: "This operator will continue to republish items coming through the
pipeline until it receives a value from another pipeline.")
Button("Open Pipeline") {
vm.startPipeline.send()
}
List(vm.data, id: \.self) { datum in
Text(datum)
}
Spacer(minLength: 0)
Button("Close Pipeline") {
vm.stopPipeline.send()
}
}
In this example, stopPipeline is a PassthroughSubject
publisher that triggers the stopping of the main pipeline.
.font(.title)
.padding(.bottom)
}
}
www.bigmountainstudio.com
208
Combine Mastery in SwiftUI
Operators
Prefix(untilOutputFrom:) - View Model
class PrefixUntilOutputFrom_IntroViewModel: ObservableObject {
@Published var data: [String] = []
var startPipeline = PassthroughSubject<Void, Never>()
var stopPipeline = PassthroughSubject<Void, Never>()
private var cancellable: AnyCancellable?
let timeFormatter = DateFormatter()
init() {
You may notice the drop(untilOutputFrom:) operator is
what turns on the flow of data. To learn more about this
operator, go here.
timeFormatter.timeStyle = .medium
cancellable = Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.drop(untilOutputFrom: startPipeline)
Once the prefix operator receives output from the
stopPipeline it will no long republish items coming through
the pipeline. This essentially shuts off the flow of data.
.prefix(untilOutputFrom: stopPipeline)
.map { datum in
return self.timeFormatter.string(from: datum)
}
.sink{ [unowned self] (datum) in
data.append(datum)
}
}
}
www.bigmountainstudio.com
209
Combine Mastery in SwiftUI
Prepend
“COMBINE AUTHORS”
“Shai”
“Donny”
“Karin”
“COMBINE AUTHORS”
The prepend operator will publish data first before the publisher send out its first item.
Note: The word “prepend” is the combination of the words “prefix” and “append”. It basically means to add something to the beginning of something else.
Operators
Prepend - Code
class Prepend_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["Karin", "Donny", "Shai", "Daniel", "Mark"]
cancellable = dataIn.publisher
.prepend("COMBINE AUTHORS")
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
No matter how many items
come through the pipeline, the
prepend operator will just run
one time to send its item
through the pipeline first.
}
}
struct Prepend_Intro: View {
@StateObject private var vm = Prepend_IntroViewModel()
Use width: 214
var body: some View {
VStack(spacing: 20) {
HeaderView("Prepend",
subtitle: "Introduction",
desc: "The prepend operator will add data before the publisher sends out
its data.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
211
Combine Mastery in SwiftUI
Operators
Prepend - Multiple
class Prepend_MultipleViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
cancellable = dataIn.publisher
.prepend("- APRIL -")
.prepend("2022")
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
This might be a little confusing
because the prepend operators at
the bottom actually publish first.
}
}
Use width: 214
struct Prepend_Multiple: View {
@StateObject private var vm = Prepend_MultipleViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Prepend",
subtitle: "Multiple",
desc: "You can have multiple prepend operators. The last prepend will be
the first published.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
.fontWeight(datum == "2022" ? .bold : .regular)
}
}
.font(.title)
.onAppear { vm.fetch() }
}
}
www.bigmountainstudio.com
212
Combine Mastery in SwiftUI
Operators
Prepend Pipelines - View
struct Prepend_Pipelines: View {
@StateObject private var vm = Prepend_PipelinesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Prepend",
subtitle: "Pipelines",
desc: "Not only can you prepend values, you can also prepend pipelines so
you get the values from another pipeline first.")
List(vm.dataToView, id: \.self) { datum in
Text(datum)
Use width: 214
.fontWeight(datum.contains("READ") ? .bold : .regular)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
The UNREAD and READ data come from two different pipelines.
You can prepend the UNREAD pipeline data to the READ pipeline.
}
See how this is done in the view model on the next page…
www.bigmountainstudio.com
213
Combine Mastery in SwiftUI
Operators
Prepend Pipelines - View Model
class Prepend_PipelinesViewModel: ObservableObject {
@Published var dataToView: [String] = []
var emails: AnyCancellable?
func fetch() {
let unread = ["New from Meng", "What Shai Mishali says about Combine"]
.publisher
.prepend("UNREAD")
let read = ["Donny Wals Newsletter", "Dave Verwer Newsletter", "Paul Hudson Newsletter"]
Here are two sources of data.
Each pipeline has its own
property.
.publisher
.prepend("READ")
emails = read
.prepend(unread)
This is where the unread
pipeline is being prepended on
the read pipeline.
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
}
}
www.bigmountainstudio.com
214
Combine Mastery in SwiftUI
Operators
Prepend Pipelines Diagram
There are a lot of prepends happening in the previous view model. Let’s see what it might look like in a diagram.
“UNREAD”
“What Shai Mishali says…” “New from Meng” “UNREAD”
“READ”
“Dave…”
“Donny Wals Newsletter”
“READ”
As soon as the UNREAD pipeline (gold) pipeline is finished, the READ pipeline will then publish its values.
🚩 Be warned though, it’s possible that the UNREAD pipeline can block the READ pipeline if it doesn’t finish. 🚩
In this example, I happen to be using Sequence publishers which automatically finish when all items have gone through the pipeline. So there’s no chance of
pipelines getting clogged or stopped by other pipelines.
www.bigmountainstudio.com
215
Combine Mastery in SwiftUI
CONTROLLING TIMING
Combine gives you operators that you can use to control the timing of data delivery. Maybe you want to delay the data delivery. Or when you get too much data, you
can control just how much of it you want to republish.
Debounce
orld!
Hello, W
Think of “debounce” like a pause. The word “bounce” is used in electrical engineering. It is when push-button switches make and break contact several times when
the button is pushed. When a user is typing and backspacing and typing more it could seem like the letters are bouncing back and forth into the pipeline.
The prefix “de-” means “to remove or lessen”. And so, “debounce” means to “lessen bouncing”. It is used to pause input before being sent down the pipeline.
􀎷
Operators
Debounce
class DebounceViewModel: ObservableObject {
@Published var name = ""
@Published var nameEntered = ""
init() {
$name
.debounce(for: 0.5, scheduler: RunLoop.main)
.assign(to: &$nameEntered)
}
}
struct Debounce_Intro: View {
@StateObject private var vm = DebounceViewModel()
Pipeline: The idea here is that we
want to “slow down” the input so
we publish whatever came into
the pipeline every 0.5 seconds.
The scheduler is basically a
mechanism to specify where and
how work is done. I’m specifying I
want work done on the main
thread. You could also use
DispatchQueue.main.
var body: some View {
VStack(spacing: 20) {
HeaderView("Debounce",
subtitle: "Introduction",
desc: "The debounce operator can pause items going through your pipeline
for a specified amount of time.")
TextField("name", text: $vm.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text(vm.nameEntered)
Spacer()
}
.font(.title)
You will notice when you play the
video that the letters entered only get
published every 0.5 seconds.
}
}
www.bigmountainstudio.com
218
Combine Mastery in SwiftUI
Operators
Debounce Flow
If you add a print() operator on the
pipeline, you will see that the data is
coming in normally from the publisher, it
is just the debounce republishes the data
every 0.5 seconds.
0.5 seconds
0.5 seconds
eykens
www.bigmountainstudio.com
Mark Mo
219
Combine Mastery in SwiftUI
Delay(for: )
You can add a delay on a pipeline to pause items from flowing through. The delay only works once though. What I mean is that if you have five items coming through
the pipeline, the delay will only pause all five and then allow them through. It will not delay every single item that comes through.
􀎷
Operators
Delay(for: ) - View
struct DelayFor_Intro: View {
@StateObject private var vm = DelayFor_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Delay(for: )",
subtitle: "Introduction",
desc: "The delay(for: ) operator will prevent the first items from
flowing through the pipeline.")
Text("Delay for:")
Picker(selection: $vm.delaySeconds, label: Text("Delay Time")) {
Text("0").tag(0)
Text("1").tag(1)
Text("2").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
Button(“Fetch Data") {
vm.fetch()
}
if vm.isFetching {
ProgressView()
} else {
Text(vm.data)
}
A ProgressView will be shown while the data is being
fetched. This is done in the view model shown on the
next page.
Spacer()
}
.font(.title)
}
}
www.bigmountainstudio.com
221
Combine Mastery in SwiftUI
Operators
Delay(for: ) - View Model
class DelayFor_IntroViewModel: ObservableObject {
@Published var data = ""
var delaySeconds = 1
@Published var isFetching = false
var cancellable: AnyCancellable?
The scheduler is basically a mechanism to
specify where and how work is done. I’m
specifying I want work done on the main
thread. You could also use:
This will show the
ProgressView on
the view.
func fetch() {
DispatchQueue.main
isFetching = true
OperationQueue.main
let dataIn = ["Value 1", "Value 2", "Value 3"]
cancellable = dataIn.publisher
.delay(for: .seconds(delaySeconds), scheduler: RunLoop.main)
.first()
.sink { [unowned self] completion in
isFetching = false
The delay can be specified in
many different ways such as:
} receiveValue: { [unowned self] firstValue in
data = firstValue
.seconds
}
.milliseconds
}
}
www.bigmountainstudio.com
.microseconds
This will hide the ProgressView
on the view.
.nanoseconds
222
Combine Mastery in SwiftUI
MeasureInterval
The measureInterval operator will tell you how much time elapsed between one item and another coming through a pipeline. It publishes the timed interval. It will
not republish the item values coming through the pipeline though.
􀎷
Operators
MeasureInterval - View
struct MeasureInterval_Intro: View {
@StateObject private var vm = MeasureInterval_IntroViewModel()
@State private var ready = false
@State private var showSpeed = false
var body: some View {
VStack(spacing: 20) {
HeaderView("MeasureInterval",
subtitle: "Introduction",
desc: "The measureInterval operator can measure how much time has elapsed
between items sent through a publisher.")
VStack(spacing: 20) {
Text("Tap Start and then tap the rectangle when it turns green")
Button("Start") {
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in:
0.5...2.0)) {
ready = true
The timeEvent property here is a
vm.timeEvent.send()
}
PassthroughSubject publisher. You can call send
}
with no value to send something down the pipeline
Button(action: {
just so we can measure the interval between.
vm.timeEvent.send()
showSpeed = true
}, label: {
RoundedRectangle(cornerRadius: 25.0).fill(ready ? Color.green :
Color.secondary)
})
Text("Reaction Speed: \(vm.speed)")
.opacity(showSpeed ? 1 : 0)
}
.padding()
}
The idea here is that once you tap the Start button, the gray
.font(.title)
shape will turn green at a random time. As soon as it turns
}
green you tap it to measure your reaction time!
}
www.bigmountainstudio.com
224
Combine Mastery in SwiftUI
Operators
MeasureInterval - View Model
class MeasureInterval_IntroViewModel: ObservableObject {
@Published var speed: TimeInterval = 0.0
var timeEvent = PassthroughSubject<Void, Never>()
private var cancellable: AnyCancellable?
The using parameter is a scheduler.
Which is basically a mechanism to specify
where and how work is done. I’m
specifying I want work done on the main
thread. You could also use:
DispatchQueue.main
OperationQueue.main
init() {
cancellable = timeEvent
.measureInterval(using: RunLoop.main)
.sink { [unowned self] (stride) in
speed = stride.timeInterval
}
}
}
Note, you could also use stride.magnitude :
The measureInterval will republish a
Stride type which is basically a form of
elapsed time.
The timeInterval property will give
you the value of this time interval
measured in seconds (and fractions of a
second as you can see in the screenshot).
Use width: 214
See if you can beat my
reaction time!
www.bigmountainstudio.com
225
Combine Mastery in SwiftUI
Throttle
If you are getting a lot of data quickly and you don’t want SwiftUI to needlessly keep redrawing your view then the throttle operator might be just the thing you’re
looking for.
You can set an interval and then republish just one value out of the many you received during that interval. For example, you can set a 2-second interval. And during
those 2 seconds, you may have received 200 values. You have the choice to republish just the most recent value received or the first value received.
􀎷
Operators
Throttle - View
struct Throttle_Intro: View {
@StateObject private var vm = Throttle_IntroViewModel()
@State private var startStop = true
var body: some View {
VStack(spacing: 20) {
HeaderView("Throttle",
subtitle: "Introduction",
desc: "Set a time interval and specify if you want the first or last item
received within that interval republished.")
.layoutPriority(1)
Text("Adjust Throttle")
Slider(value: $vm.throttleValue, in: 0.1...1,
minimumValueLabel: Image(systemName: "hare"),
maximumValueLabel: Image(systemName: "tortoise"),
label: { Text("Throttle") })
.padding(.horizontal)
HStack {
Button(startStop ? "Start" : "Stop") {
startStop.toggle()
vm.start()
}
.frame(maxWidth: .infinity)
Button("Reset") { vm.reset() }
.frame(maxWidth: .infinity)
}
This button will toggle from
Start to Stop. We’re calling the
same start function on the view
model though so it will handle
turning the pipeline on or off.
List(vm.data, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
}
}
www.bigmountainstudio.com
227
Combine Mastery in SwiftUI
Operators
Throttle - View Model
class Throttle_IntroViewModel: ObservableObject {
@Published var data: [String] = []
var throttleValue: Double = 0.5
private var cancellable: AnyCancellable?
let timeFormatter = DateFormatter()
For this example, I’m using a Timer
publisher to emit values every 0.1
seconds.
init() {
timeFormatter.dateFormat = "HH:mm:ss.SSS"
}
The latest option lets you republish
func start() {
the last one if true or the first one
if (cancellable != nil) {
during the interval if false.
cancellable = nil
} else {
cancellable = Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.throttle(for: .seconds(throttleValue), scheduler: RunLoop.main, latest: true)
.map { [unowned self] datum in
timeFormatter.string(from: datum)
}
.sink{ [unowned self] (datum) in
The scheduler is basically
data.append(datum)
a mechanism to specify
}
The interval can be
where and how work is
}
specified in many
done. I’m specifying I want
}
different ways such as:
func reset() {
data.removeAll()
}
}
www.bigmountainstudio.com
.seconds
.milliseconds
.microseconds
.nanoseconds
Use width: 214
work done on the main
thread. You could also use:
DispatchQueue.main
OperationQueue.main
228
Combine Mastery in SwiftUI
Timeout
error
You don’t want to make users wait too long while the app is retrieving or processing data. So you can use the timeout operator to set a time limit. If the pipeline takes
too long you can automatically finish it once the time limit is hit. Optionally, you can define an error so you can look for this error when the pipeline finishes.
This way when the pipeline finishes, you can know if it was specifically because of the timeout and not because of some other condition.
Operators
Timeout - View
struct Timeout_Intro: View {
@StateObject private var vm = Timeout_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Timeout",
subtitle: “Introduction",
desc: "You can specify a time limit for the timeout operator. If no item
comes down the pipeline before that time limit then pipeline is
finished.")
Button("Fetch Data") {
vm.fetch()
}
if vm.isFetching {
ProgressView("Fetching...")
}
Use width: 214
Spacer()
DescView("You can also set a custom error when the time limit is exceeded.")
Spacer()
}
.font(.title)
.alert(item: $vm.timeoutError) { timeoutError in
Alert(title: Text(timeoutError.title), message: Text(timeoutError.message))
}
}
}
www.bigmountainstudio.com
230
Combine Mastery in SwiftUI
Operators
Timeout - View Model
class Timeout_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var isFetching = false
This URL isn’t real. I wanted
something that would delay fetching.
@Published var timeoutError: TimeoutError?
private var cancellable: AnyCancellable?
Learn more about the
dataTaskPublisher here.
func fetch() {
isFetching = true
let url = URL(string: "https://bigmountainstudio.com/nothing")!
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.timeout(.seconds(0.1), scheduler: RunLoop.main, customError: { URLError(.timedOut) })
.map { $0.data }
.decode(type: String.self, decoder: JSONDecoder())
I set the timeout to be super short
(0.1 seconds) just to trigger it.
.sink(receiveCompletion: { [unowned self] completion in
Use width: 214
isFetching = false
if case .failure(URLError.timedOut) = completion {
timeoutError = TimeoutError()
}
}, receiveValue: { [unowned self] value in
dataToView.append(value)
})
}
}
www.bigmountainstudio.com
struct TimeoutError: Error, Identifiable {
let id = UUID()
let title = "Timeout"
let message = "Please try again later."
}
231
The scheduler is basically
a mechanism to specify
where and how work is
done. I’m specifying I want
work done on the main
thread. You could also use:
DispatchQueue.main
OperationQueue.main
Combine Mastery in SwiftUI
FILTERING ELEMENTS
!=
These operators give you ways to decide which items get published and which ones do not.
CompactMap
nil value5 nil value4
ni value3
l
ni
l
value2
value1
The compactMap operator gives you a convenient way to drop all nils that come through the pipeline. You are even given a closure to evaluate items coming through
the pipeline and if you want, you can return a nil. That way, the item will also get dropped. (See example on the following pages.)
Operators
CompactMap - View
struct CompactMap_Intro: View {
@StateObject private var vm = CompactMap_IntroViewModel()
var body: some View {
VStack(spacing: 10) {
HeaderView("CompactMap",
subtitle: "Introduction",
desc: "The compactMap operator will remove nil values as they come
through the pipeline.")
.layoutPriority(1)
Text("Before using compactMap:")
List(vm.dataWithNils, id: \.self) { item in
Text(item)
.font(.title3)
.foregroundColor(.gray)
}
Use width: 214
Text("After using compactMap:")
List(vm.dataWithoutNils, id: \.self) { item in
Text(item)
.font(.title3)
.foregroundColor(.gray)
}
.frame(maxHeight: 150)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
Let’s look at the pipeline and see what
happened on the next page.
}
www.bigmountainstudio.com
Looking at the screenshot of before and
after compactMap, you can see that the
nils were dropped. But you also see that
“Invalid” was dropped too.
234
Combine Mastery in SwiftUI
Operators
CompactMap - View Model
class CompactMap_IntroViewModel: ObservableObject {
@Published var dataWithNils: [String] = []
@Published var dataWithoutNils: [String] = []
func fetch() {
let dataIn = ["Value 1", nil, "Value 3", nil, "Value 5", "Invalid"]
_ = dataIn.publisher
.sink { [unowned self] (item) in
dataWithNils.append(item ?? "nil")
}
_ = dataIn.publisher
.compactMap{ item in
if item == "Invalid" {
return nil // Will not get republished
}
return item
}
.sink { [unowned self] (item) in
dataWithoutNils.append(item)
}
“Invalid” was dropped because inside our
compactMap we look for this value in
particular and return a nil.
Returning a nil inside a compactMap
closure means it will get dropped.
}
}
Are nils passed into compactMap?
Shorthand Argument Names
Actually, yes. Nils will come in and can be returned from
the closure but they do not continue down the pipeline.
If you don’t have any logic then you can use
shorthand argument names like this:
.compactMap { $0 }
www.bigmountainstudio.com
235
Combine Mastery in SwiftUI
TryCompactMap
error
nil value5 nil value4
ni value3
l
ni
l
value2
value1
Just like the compactMap except you are also allowed to throw an error inside the closure provided. This operator lets the pipeline know that a failure is possible. So
when you add a sink subscriber, the pipeline will only allow you to add a sink(receiveCompletion:receiveValue:) as it expects you to handle possible failures.
Operators
TryCompactMap - View
struct TryCompactMap_Intro: View {
@StateObject private var vm = TryCompactMap_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryCompactMap",
subtitle: "Introduction",
desc: "Use tryCompactMap to remove nils but also have the option to throw
an error.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
This is an error type in the view model that
also conforms to Identifiable so it can
be used here as the item parameter.
}
}
.font(.title)
.alert(item: $vm.invalidValueError) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
.onAppear {
vm.fetch()
}
Like all other operators that begin with
“try”, tryCompactMap lets the pipeline
know that a possible failure is possible.
}
}
www.bigmountainstudio.com
237
Combine Mastery in SwiftUI
Operators
TryCompactMap - View Model
struct InvalidValueError: Error, Identifiable {
let id = UUID()
let description = "One of the values you entered is invalid and will have to be updated."
}
The error conforms to Identifiable
so the @Published property can be
observed by the alert modifier on
the previous page.
class TryCompactMap_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var invalidValueError: InvalidValueError?
func fetch() {
let dataIn = ["Value 1", nil, "Value 3", nil, "Value 5", "Invalid"]
_ = dataIn.publisher
.tryCompactMap{ item in
if item == "Invalid" {
throw InvalidValueError()
}
In this scenario, we throw an error instead
of dropping the item by returning a nil.
(See previous example at compactMap.)
return item
}
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.invalidValueError = error as? InvalidValueError
}
} receiveValue: { [unowned self] (item) in
dataToView.append(item)
}
}
Since the tryCompactMap indicates a failure can occur in
the pipeline, you are forced to use the
sink(receiveCompletion:receiveValue:)
subscriber.
Xcode will complain if you just try to use the
sink(receiveValue:) subscriber.
}
www.bigmountainstudio.com
238
Combine Mastery in SwiftUI
Filter
❌
✅
❌
⭐
==
✅✅
?
✅
✅
✅
✅
Use this operator to specify which items get republished based on the criteria you set up. You may have a scenario where you have data cached or in memory. You
can use this filter operator to return all the items that match the user’s criteria and republish that data to the UI.
􀎷
Operators
Filter - View
struct Filter_Introduction: View {
@StateObject private var vm = Filter_IntroductionViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Filter",
subtitle: "Introduction",
desc: "The filter operator will republished upstream values it receives
if it matches some criteria that you specify.")
HStack(spacing: 40.0) {
Button("Animals") { vm.filterData(criteria: "Animal") }
Button("People") { vm.filterData(criteria: "Person") }
Button("All") { vm.filterData(criteria: " ") }
}
List(vm.filteredData, id: \.self) { datum in
Text(datum)
}
}
Any data that match the criteria will be allowed to continue down the
pipeline.
✅ ❌
.font(.title)
}
❌ ✅
}
❌
www.bigmountainstudio.com
240
✅
❌
==
✅
?
✅
✅ ✅
✅
Combine Mastery in SwiftUI
Operators
Filter - View Model
class Filter_IntroductionViewModel: ObservableObject {
@Published var filteredData: [String] = []
let dataIn = ["Person 1", "Person 2", "Animal 1", "Person 3", "Animal 2", "Animal 3"]
private var cancellable: AnyCancellable?
init() {
filterData(criteria: " ")
}
In this scenario, we pretend we already
have some fetched data we’re working
with (dataIn).
(Most likely your fetch function will populate the
filteredData property. This is in here just to
get it initially populated.)
func filterData(criteria: String) {
filteredData = []
cancellable = dataIn.publisher
.filter { item -> Bool in
item.contains(criteria)
}
.sink { [unowned self] datum in
Every item that comes through the
pipeline will be checked against your
criteria.
If true, the filter operator republishes the
data and it continues down the pipeline.
filteredData.append(datum)
}
}
}
Shorthand Argument Names
If you don’t have any logic then you can use
shorthand argument names like this:
.filter { $0.contains(criteria) }
www.bigmountainstudio.com
241
Combine Mastery in SwiftUI
TryFilter
error
❌
✅
❌
⭐
==
✅✅
?
✅
✅
The tryFilter operator works just like the filter operator except it also allows you to throw an error within the closure.
✅
✅
Operators
TryFilter - View
struct TryFilter_Intro: View {
@StateObject private var vm = TryFilter_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryFilter",
subtitle: "Introduction",
desc: "The tryFilter operator will republished items that match your
criteria or can throw an error that will cancel the pipeline.")
HStack(spacing: 40.0) {
Button("Animals") { vm.filterData(criteria: "Animal") }
Button("People") { vm.filterData(criteria: "Person") }
Use width: 214
Button("All") { vm.filterData(criteria: " ") }
}
List(vm.filteredData, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
This example works like the previous
example except now an alert will be
displayed if the filterError published
property on the view model becomes not nil.
.alert(item: $vm.filterError) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
}
}
www.bigmountainstudio.com
243
Combine Mastery in SwiftUI
Operators
TryFilter - View Model
struct FilterError: Error, Identifiable {
let id = UUID()
let description = "There was a problem filtering. Please try again."
}
The error conforms to Identifiable so
the @Published property can be observed
by the alert modifier on the previous
page.
class TryFilter_IntroViewModel: ObservableObject {
@Published var filteredData: [String] = []
@Published var filterError: FilterError?
let dataIn = ["Person 1", "Person 2", "Animal 1", "Person 3", "Animal 2", "Animal 3", "🧨 "]
private var cancellable: AnyCancellable?
init() {
filterData(criteria: " ")
}
func filterData(criteria: String) {
filteredData = []
cancellable = dataIn.publisher
.tryFilter { item -> Bool in
if item == "🧨 " {
throw FilterError()
}
In this scenario, we throw an error. The sink subscriber will catch it and
assign it to a @Published property. Once that happens the view will show
an alert with the error message.
return item.contains(criteria)
Since the tryFilter indicates a failure can occur in the
pipeline, you are forced to use the
sink(receiveCompletion:receiveValue:)
subscriber.
}
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.filterError = error as? FilterError
}
} receiveValue: { [unowned self] (item) in
filteredData.append(item)
}
Xcode will complain if you just try to use the
sink(receiveValue:) subscriber.
}
}
www.bigmountainstudio.com
244
Combine Mastery in SwiftUI
RemoveDuplicates
==
Your app may subscribe to a feed of data that could give you repeated values. Imagine a weather app for example that periodically checks the temperature. If your
app keeps getting the same temperature then there may be no need to send it through the pipeline and update the UI.
The removeDuplicates could be a solution so your app only responds to data that has changed rather than getting duplicate data. If the data being sent through
the pipeline conforms to the Equatable protocol then this operator will do all the work of removing duplicates for you.
Operators
RemoveDuplicates
class RemoveDuplicatesViewModel: ObservableObject {
@Published var data: [String] = []
var cancellable: AnyCancellable?
func fetch() {
let dataIn = ["Lem", "Lem", "Scott", "Scott", "Chris", "Mark", "Adam", "Jared", "Mark"]
cancellable = dataIn.publisher
.removeDuplicates()
.sink{ [unowned self] datum in
self.data.append(datum)
}
}
}
If an item coming through the
pipeline was the same as the
previous element, the
removeDuplicates operator
will not republish it.
struct RemoveDuplicates_Intro: View {
@StateObject private var vm = RemoveDuplicatesViewModel()
Use width: 214
var body: some View {
VStack(spacing: 20) {
HeaderView("Remove Duplicates",
subtitle: "Introduction",
desc: "If any repeated data is found, it will be removed.")
ScrollView {
ForEach(vm.data, id: \.self) { name in
Text(name)
.padding(-1)
Divider()
}
}
DescView("Notice that only duplicates that are one-after-another are removed.")
}
.font(.title)
.onAppear { vm.fetch() }
}
}
www.bigmountainstudio.com
246
Combine Mastery in SwiftUI
RemoveDuplicates(by:)
1
12
12
12
12
==
1
== 1 2
12
12
12
The removeDuplicates(by:) operator works like the removeDuplicates operator but for objects that do not conform to the Equatable protocol. (Objects that
conform to the Equatable protocol can be compared in code to see if they are equal or not.)
Since removeDuplicates won’t be able to tell if the previous item is the same as the current item, you can specify what makes the two items equal inside this closure.
Operators
RemoveDuplicates(by:) - View
struct RemoveDuplicatesBy_Intro: View {
@StateObject private var vm = RemoveDuplicatesBy_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("RemoveDuplicates(by: )",
subtitle: "Introduction",
desc: "Combine provides you a way to remove duplicate objects that do not
conform to Equatable using the removeDuplicates(by: ) operator in
which you supply your own criteria.")
.layoutPriority(1)
Use width: 214
List(vm.dataToView) { item in
Text(item.email)
These email addresses are part of a struct
that does not conform to Equatable. So
the pipeline uses
removeDuplicates(by:) so it can
determine which objects are equal or not.
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
248
Combine Mastery in SwiftUI
Operators
RemoveDuplicates(by: ) - View Model
struct UserId: Identifiable {
let id = UUID()
var email = ""
var name = ""
}
class RemoveDuplicatesBy_IntroViewModel: ObservableObject {
@Published var dataToView: [UserId] = []
func fetch() {
let dataIn = [UserId(email: "joe.m@gmail.com", name: "Joe M."),
UserId(email: "joe.m@gmail.com", name: "Joseph M."),
UserId(email: "christina@icloud.com", name: "Christina B."),
UserId(email: "enzo@enel.it", name: "Lorenzo D."),
UserId(email: "enzo@enel.it", name: "Enzo D.")]
_ = dataIn.publisher
.removeDuplicates(by: { (previousUserId, currentUserId) -> Bool in
previousUserId.email == currentUserId.email
})
If the email addresses are the same, we are
going to consider that it is the same user and
that is what makes UserId structs equal.
.sink { [unowned self] (item) in
dataToView.append(item)
}
}
}
Shorthand Argument Names
Note: An even shorter way to write this is to use
shorthand argument names like this:
.removeDuplicates { $0.email == $1.email }
www.bigmountainstudio.com
249
Combine Mastery in SwiftUI
TryRemoveDuplicates
error
1
12
12
12
12
==
1
== 1 2
12
12
12
You will find the tryRemoveDuplicates is just like the removeDuplicates(by:) operator except it also allows you to throw an error within the closure. In the
closure where you set your condition on what is a duplicate or not, you can throw an error if needed and the subscriber (or other operators) will then handle the
error.
Operators
TryRemoveDuplicates - View
struct TryRemoveDuplicates: View {
@StateObject private var vm = TryRemoveDuplicatesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryRemoveDuplicates",
subtitle: "Introduction",
desc: "The tryRemoveDuplicates(by: ) operator will drop duplicate objects
that match the criteria you specify and can also throw an error.")
List(vm.dataToView) { item in
Use width: 214
Text(item.email)
}
}
.font(.title)
This example works like the previous
example except now an alert will be
displayed if the removeDuplicateError
published property on the view model
becomes not nil.
.alert(item: $vm.removeDuplicateError) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
251
Combine Mastery in SwiftUI
Operators
TryRemoveDuplicates - View Model
struct RemoveDuplicateError: Error, Identifiable {
let id = UUID()
let description = "There was a problem removing duplicate items."
}
class TryRemoveDuplicatesViewModel: ObservableObject {
@Published var dataToView: [UserId] = []
@Published var removeDuplicateError: RemoveDuplicateError?
func fetch() {
let dataIn = [UserId(email:
UserId(email:
UserId(email:
UserId(email:
UserId(email:
"joe.m@gmail.com", name: "Joe M."),
"joe.m@gmail.com", name: "Joseph M."),
"christina@icloud.com", name: "Christina B."),
"N/A", name: "N/A"),
"N/A", name: "N/A")]
_ = dataIn.publisher
.tryRemoveDuplicates(by: { (previousUserId, currentUserId) -> Bool in
if (previousUserId.email == "N/A" && currentUserId.email == "N/A") {
throw RemoveDuplicateError()
}
return previousUserId.email == currentUserId.email
})
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.removeDuplicateError = error as? RemoveDuplicateError
}
} receiveValue: { [unowned self] (item) in
dataToView.append(item)
}
}
In this scenario, we throw an error. The sink
subscriber will catch it and assign it to a
@Published property. Once that happens the
view will show an alert with the error message.
Since the tryRemoveDuplicates indicates a failure can
occur in the pipeline, you are forced to use the
sink(receiveCompletion:receiveValue:)
subscriber.
Xcode will complain if you just try to use the
sink(receiveValue:) subscriber.
}
www.bigmountainstudio.com
The error conforms to Identifiable so
the @Published property can be observed
by the alert modifier on the previous
page.
252
Combine Mastery in SwiftUI
ReplaceEmpty
?
Use the replaceEmpty operator when you want to show or set some value in the case that nothing came down your pipeline. This could be useful in situations
where you want to set some default data or notify the user that there was no data.
􀎷
Operators
ReplaceEmpty - View
struct ReplaceEmpty: View {
@StateObject private var vm = ReplaceEmptyViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("ReplaceEmpty",
subtitle: "Introduction",
desc: "You can use replaceEmpty in cases where you have a publisher that
finishes and nothing came down the pipeline.")
HStack {
TextField("criteria", text: $vm.criteria)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Search") {
vm.search()
If no data was returned, then a
check is done and the color of the
text is changed here.
}
}
.padding()
List(vm.dataToView, id: \.self) { item in
Text(item)
.foregroundColor(item == vm.noResults ? .gray : .primary)
}
}
.font(.title)
}
}
www.bigmountainstudio.com
254
Combine Mastery in SwiftUI
Operators
ReplaceEmpty - View Model
class ReplaceEmptyViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var criteria = ""
var noResults = "No results found"
func search() {
dataToView.removeAll()
let dataIn = ["Result 1", "Result 2", "Result 3", "Result 4"]
Learn more about how the
filter operator works.
_ = dataIn.publisher
.filter { $0.contains(criteria) }
.replaceEmpty(with: noResults)
.sink { [unowned self] (item) in
dataToView.append(item)
}
If the pipeline finishes and nothing came through it (no matches found), then
the value defined in the replaceEmpty operator will be published.
Note: This will only work on a pipeline that actually finishes. In this scenario, a
Sequence publisher is being used and it will finish by itself when all items have run
through the pipeline.
}
}
www.bigmountainstudio.com
255
Combine Mastery in SwiftUI
MAPPING ELEMENTS
"
"
"
"
"
!
!
!
!
These operators all have to do with performing some function on each item coming through the pipeline. The function or process you want to do with each element
can be anything from validating the item to changing it into something else.
Map
With the map operator, you provide the code to perform on each item coming through the pipeline. With the map function, you can inspect items coming through and
validate them, update them to something else, even change the type of the item.
Maybe your map operator receives a tuple (a type that holds two values) but you only want one value out of it to continue down the pipeline. Maybe it receives Ints
but you want to convert them to Strings. This is an operator in which you can do anything you want within it. This makes it a very popular operator to know.
Operators
Map - View
struct Map_Intro: View {
@StateObject private var vm = Map_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Map",
subtitle: "Introduction",
desc: "Use the map operator to run some code with each item that is
passed through the pipeline.")
List(vm.dataToView, id: \.self) { item in
Text(item)
Use width: 214
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
Every item that goes through the
pipeline will get an icon added to it
and be turned to uppercase.
www.bigmountainstudio.com
258
Combine Mastery in SwiftUI
Operators
Map - View Model
class Map_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["mark", "karin", "chris", "ellen", "paul", "scott"]
_ = dataIn.publisher
.map({ (item) in
Map operators receive an item, do something to it, and then
republish an item. Something always needs to be returned to
continue down the pipeline.
return "*⃣ " + item.uppercased()
})
.sink { [unowned self] (item) in
dataToView.append(item)
}
}
}
Simplification
Many times you will see closures like this simplified to different degrees. Here are some examples:
Parentheses Removed
.map { item in
return "*⃣ " + item.uppercased()
}
You can remove the parentheses and the
code will still compile just fine.
www.bigmountainstudio.com
Return Removed
.map { item in
"*⃣ " + item.uppercased()
}
The return keyword is now optional if only
one line in the closure.
259
Using Shorthand Argument Names
.map { "*⃣ " + $0.uppercased() }
Also called “anonymous closure arguments”, use
$0 to refer to the first parameter passed into the
closure. More info here.
Combine Mastery in SwiftUI
Operators
Map: Key Path - View
struct Map_Keypath: View {
@StateObject private var vm = Map_KeypathViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Map",
subtitle: “Key Path",
desc: "You can also use the map operator to get a single property out of
an object by using a key path.”)
Text("Creators")
.bold()
Use width: 214
List(vm.dataToView, id: \.self) { item in
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
In this example, a data object is being sent
down the pipeline but only one property
from that data object is needed on the UI.
So map uses a key path to access just that
one property.
}
}
}
www.bigmountainstudio.com
260
Combine Mastery in SwiftUI
Operators
Map: Key Path - View Model
struct Creator: Identifiable {
let id = UUID()
var fullname = ""
This is the object sent down the pipeline.
}
?
class Map_KeypathViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = [
What is a key path?
Creator(fullname: "Mark Moeykens"),
Creator(fullname: "Karin Prater"),
A “key path” is a way to get to a property
in an object (struct, class, etc.).
Creator(fullname: "Chris Ching"),
Creator(fullname: "Donny Wals"),
Creator(fullname: "Paul Hudson"),
Maybe it would make more sense if we
called it a “property path”.
Creator(fullname: "Joe Heck")]
_ = dataIn.publisher
You simply provide a key path to the property
that you want to send downstream.
.map(\.fullname)
.sink { [unowned self] (name) in
dataToView.append(name)
Note: You can also used a shorthand argument
name too: .map { $0.fullname }
The map operator will use these
directions to find the property, get the
value, and then send that value
downstream.
}
}
}
www.bigmountainstudio.com
It does not return a value from a property,
rather it provides directions on how to
find it.
261
Combine Mastery in SwiftUI
TryMap
error
The tryMap operator is just like the map operator except it can throw errors. Use this if you believe items coming through could possibly cause an error. Errors
thrown will finish the pipeline early.
Operators
TryMap - View
struct TryMap_Intro: View {
@StateObject private var vm = TryMap_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryMap",
subtitle: "Introduction",
desc: "The tryMap operator will allow you to throw an error inside its
closure.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
263
Combine Mastery in SwiftUI
Operators
TryMap - View Model
struct ServerError: Error, Identifiable, CustomStringConvertible {
let id = UUID()
let description = "There was a server error while retrieving values."
}
This will be the error type thrown in the tryMap.
Identifiable
The error conforms to Identifiable so the view’s alert
modifier can observe it and display an Alert.
class TryMap_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var error: ServerError?
CustomStringConvertible
func fetch() {
let dataIn = ["Value 1", "Value 2", "Server Error 500", "Value 3"]
_ = dataIn.publisher
.tryMap { item -> String in
if item.lowercased().contains("error") {
throw ServerError()
}
This allows us to set a description for our error object
that we can then use on the UI. You could just as easily
add your own String property to hold an error message.
Sink
There are two sink subscribers:
1. sink(receiveValue:)
2. sink(receiveCompletion:receiveValue:)
return item
}
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = error as? ServerError
}
} receiveValue: { [unowned self] item in
dataToView.append(item)
}
When it comes to this pipeline, we are forced to use the second one
because this pipeline can fail. Meaning the publisher and other
operators can throw an error.
Xcode’s autocomplete won’t even show you the first option for this
pipeline so you don’t have to worry about which one to pick.
}
Handling Errors
}
For more information on options, look at the chapter Handling Errors.
www.bigmountainstudio.com
264
Combine Mastery in SwiftUI
ReplaceNil
“N/A”
“Customer Three”
“Customer Two”
nil
“Customer One”
It’s possible you might get nils in data that you fetch. You can have Combine replace nils with a value you specify.
Operators
ReplaceNil
class ReplaceNil_IntroViewModel: ObservableObject {
@Published var data: [String] = []
private var cancellable: AnyCancellable?
init() {
let dataIn = ["Customer 1", nil, nil, "Customer 2", nil, "Customer 3"]
cancellable = dataIn.publisher
.replaceNil(with: "N/A")
.sink { [unowned self] datum in
self.data.append(datum)
}
You couldn’t ask for an easier operator. 😃
}
}
struct ReplaceNil_Intro: View {
@StateObject private var vm = ReplaceNil_IntroViewModel()
Use width: 214
var body: some View {
VStack(spacing: 20) {
HeaderView("Replace Nil",
subtitle: "Introduction",
desc: "If you know you will get nils in your stream, you have the option
to use the replaceNil operator to replace those nils with another
value.")
List(vm.data, id: \.self) { datum in
Text(datum)
}
DescView("In this example, I'm replacing nils with 'N/A'.")
}
.font(.title)
}
}
www.bigmountainstudio.com
266
Combine Mastery in SwiftUI
SetFailureType
error
There are two types of pipelines. Pipelines that have publishers/operators that can throw errors and those that do not. The setFailureType is for those pipelines that
do not throw errors. This operator doesn’t actually throw an error and it will not cause an error to be thrown later. It does not affect your pipeline in any way other
than to change the type of your pipeline. Read more on the next page to understand what this means.
Operators
SetFailureType - Two Types of Pipelines
To understand when to use setFailureType, first look at the two types of pipelines.
Error-Throwing Pipeline
Non-Error-Throwing Pipeline
let errorPipeline: AnyPublisher<String, Error> =
let pipeline: AnyPublisher<String, Never> =
["Utah", "Nevada", "Colorado", "🧨 ", "Idaho"].publisher
["Utah", "Nevada", "Colorado", "🧨 ", "Idaho"].publisher
.tryMap { item -> String in
.map { item -> String in
if item == "🧨 " {
if item == "🧨 " {
throw InvalidValueError()
return "Montana"
}
return item
}
return item
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
To learn more about
AnyPublisher and
The eraseToAnyPublisher operator allows
you to simplify the type of your publishers.
So what are the differences you see between
these two pipelines?
They are pretty similar except the first one throws
an error. So the pipeline’s failure type is set to
Error and the second one is set to Never.
www.bigmountainstudio.com
To see an example of how publisher types
“nest” and get complex, see this page.
eraseToAnyPublisher,
look at the chapter
“Organizing”.
Also see:
map
tryMap
268
Combine Mastery in SwiftUI
Operators
SetFailureType - Problem
Now imagine you want a function that can return either one of these pipelines. They are different types, right? You need a way to make it so their types match up.
func getPipeline(westernStates: Bool) -> AnyPublisher<String, Error> {
if westernStates {
return
["Utah", "Nevada", "Colorado", "🧨 ", "Idaho"].publisher
This publisher matches the return type.
.tryMap { item -> String in
if item == "🧨 " {
throw InvalidValueError()
}
return item
}
.eraseToAnyPublisher()
} else {
return
["Vermont", "New Hampshire", "Maine", "🧨 ", "Rhode Island"].publisher
This publisher’s type is: AnyPublisher<String, Never>
Even though it will never return an error, this is where you use
setFailureType on the pipeline so it can match the return
type of this function.
.map { item -> String in
if item == "🧨 " {
return "New Hampshire"
}
return item
Now both publishers match because setFailureType
changed the type to: AnyPublisher<String, Error>
}
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
www.bigmountainstudio.com
269
Combine Mastery in SwiftUI
􀎷
Operators
SetFailureType - View
struct SetFailureType_Intro: View {
@StateObject private var vm = SetFailureType_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("SetFailureType",
subtitle: "Introduction",
desc: "The setFailureType operator can change a type of a publisher by
changing its failure type from Never to something else.")
HStack(spacing: 50) {
Button("Western") { vm.fetch(westernStates: true) }
Button("Eastern") { vm.fetch(westernStates: false) }
}
Text("States")
.bold()
List(vm.states, id: \.self) { state in
Text(state)
}
Both buttons will call the same function. Two
different publishers are used to get the states.
The Western publisher throws an error. The
Eastern publisher does not.
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}
www.bigmountainstudio.com
270
Combine Mastery in SwiftUI
Operators
SetFailureType - View Model
class SetFailureType_IntroViewModel: ObservableObject {
@Published var states: [String] = []
@Published var error: ErrorForAlert?
The error needs to conform to Identifiable because it is
needed to work with the SwiftUI alert modifier:
func getPipeline(westernStates: Bool) -> AnyPublisher<String, Error> {
if westernStates {
return
["Utah", "Nevada", "Colorado", "🧨 ", "Idaho"].publisher
struct ErrorForAlert: Error, Identifiable {
let id = UUID()
let title = "Error"
var message = "Please try again later."
}
.tryMap { item -> String in
if item == "🧨 " {
throw ErrorForAlert()
}
return item
}
.eraseToAnyPublisher()
} else {
return
["Vermont", "New Hampshire", "Maine", "🧨 ", "Rhode Island"].publisher
.map { item -> String in
if item == "🧨 " {
return "Massachusetts"
}
return item
The setFailureType is used to make this pipeline errorthrowing to match the first publisher.
}
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
This observable object continues on the next page
where you can see the fetch function.
}
www.bigmountainstudio.com
You have a choice here. You can either make both
publishers error-throwing or make both non-errorthrowing.
271
Combine Mastery in SwiftUI
Operators
func fetch(westernStates: Bool) {
states.removeAll()
Once you have a publisher, all you need to do is to attach a subscriber.
_ = getPipeline(westernStates: westernStates)
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
self.error = error as? ErrorForAlert
}
} receiveValue: { [unowned self] (state) in
states.append(state)
}
Because the type returned specifies the possible failure of Error instead of
Never, it is an error-throwing pipeline.
Xcode will force you to use sink(receiveCompletion:receiveValue:) for
error-throwing pipelines.
(Non-error-throwing pipelines can use either sink(receiveValue:) or
assign(to:). )
}
}
To learn more about the errorthrowing pipelines and how to
convert them to non-error-throwing
pipelines, see the chapter on
“Handling Errors”.
www.bigmountainstudio.com
272
Combine Mastery in SwiftUI
Scan
last value:
7
6
5
123
current value:
4
123
12
1
The scan operator gives you the ability to see the item that was previously returned from the scan closure along with the current one. That is all the operator does.
From here it is up to you with how you want to use this. In the image above, the current value is appended to the last value and sent down the pipeline.
Operators
Scan - View
struct Scan_Intro: View {
@StateObject private var vm = Scan_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Scan",
subtitle: "Introduction",
desc: "The scan operator allows you to access the previous item that it
had returned.")
List(vm.dataToView, id: \.self) { datum in
Use width: 214
Text(datum)
}
}
.font(.title)
In this example, I am connecting the current item coming through the
pipeline with the previous item. Then I publish that as a new item.
.onAppear {
vm.fetch()
When the next item comes through, I attach that previous item again.
}
Although I’m connecting items as they come through the pipeline, you
don’t have to use scan for this purpose. The main purpose of the
scan operator is to give you is the ability to examine the previous
item that was published.
}
}
www.bigmountainstudio.com
274
Combine Mastery in SwiftUI
Operators
Scan - View Model
class Scan_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
The first time an item comes
through the scan closure
there will be no previous item.
So you can provide an initial
value to use.
func fetch() {
let dataIn = ["1⃣ ", "2⃣ ", "3⃣ ", "4⃣ ", "5⃣ ", "6⃣ ", "7⃣ "]
_ = dataIn.publisher
.scan("0⃣ ") { (previousReturnedValue, currentValue) in
previousReturnedValue + " " + currentValue
}
What you return from scan
becomes available to look at the
next time the current item
comes through this closure.
Use width: 214
.sink { [unowned self] (item) in
dataToView.append(item)
}
}
}
Shorthand Argument Names
Note: An even shorter way to write this is to use
shorthand argument names like this:
.scan("0⃣ ") { $0 + " " + $1 }
www.bigmountainstudio.com
275
Combine Mastery in SwiftUI
TryScan
error
last value:
7
!
5
123
current value:
4
123
12
1
The tryScan operator works just like the scan operator, it allows you to examine the last item that the scan operator’s closure returned. In addition to that, it allows
you to throw an error. Once this happens the pipeline will finish.
Operators
TryScan - View
struct TryScan: View {
@StateObject private var vm = TryScanViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryScan",
subtitle: "Introduction",
desc: "The tryScan operator will do the same thing as the scan operator
but it also has the ability to throw errors. If an error is
thrown, the pipeline will finish.")
Use width: 214
List(vm.dataToView, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
Instead of handling the error with an alert, a message
is published so it gets appended to the data.
See how I am doing this on the next page.
www.bigmountainstudio.com
277
Combine Mastery in SwiftUI
Operators
TryScan - View Model
class TryScanViewModel: ObservableObject {
@Published var dataToView: [String] = []
When the publisher sends a 🧨 down the pipeline, an
private let invalidValue = "🧨 "
error will be thrown from the tryScan and handled in
the sink.
func fetch() {
let dataIn = ["1⃣ ", "2⃣ ", "3⃣ ", "4⃣ ", "🧨 ", "5⃣ ", "6⃣ ", "7⃣ "]
_ = dataIn.publisher
.tryScan("0⃣ ") { [unowned self] (previousReturnedValue, currentValue) in
if currentValue == invalidValue {
struct InvalidValueFoundError: Error {
let message = "Invalid value was found: "
}
throw InvalidValueFoundError()
}
return previousReturnedValue + " " + currentValue
}
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
if let err =
error as? InvalidValueFoundError {
dataToView.append(err.message + invalidValue)
}
The error message is just being appended to our data
to be displayed on the view.
}
} receiveValue: { [unowned self] (item) in
dataToView.append(item)
}
}
}
www.bigmountainstudio.com
278
Combine Mastery in SwiftUI
REDUCING ELEMENTS
These operators focus on grouping items, removing items, or narrowing down items that come through a pipeline down to just one item.
Collect
[
]
The collect operator won’t let items pass through the pipeline. Instead, it will put all items into an array, and then when the pipeline finishes it will publish the
array.
􀎷
Operators
Collect - View
struct Collect_Intro: View {
@StateObject private var vm = Collect_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Collect",
subtitle: "Introduction",
desc: "This operator collects values into an array. When the pipeline
finishes, it publishes the array.")
Toggle("Circles", isOn: $vm.circles)
.padding()
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 200))]) {
ForEach(vm.dataToView, id: \.self) { item in
Image(systemName: item)
}
}
Spacer(minLength: 0)
}
.font(.title)
In this example, we run through 25 numbers and
arrange them in a lazy grid.
.onAppear {
vm.fetch()
}
If the Circles toggle is changed then the pipeline that
composes all of the image names is run again.
}
}
www.bigmountainstudio.com
281
Combine Mastery in SwiftUI
Operators
Collect - View Model
class Collect_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var circles = false
private var cachedData: [Int] = []
private var cancellables: Set<AnyCancellable> = []
init() {
$circles
.sink { [unowned self] shape in formatData(shape: shape ? "circle" : "square") }
.store(in: &cancellables)
}
You will find that collect is great for SwiftUI
because you can then use the assign(to:) subscriber.
This means you don’t need to store a cancellable.
func fetch() {
cachedData = Array(1...25)
If you were to do this without using collect, it
would look something like this:
formatData(shape: circles ? "circle" : "square")
}
func formatData(shape: String) {
dataToView.removeAll()
func formatData(shape: String) {
cachedData.publisher
cachedData.publisher
.map { "\($0).\(shape)" }
.sink { [unowned self] item in
dataToView.append(item)
}
.store(in: &cancellables)
.map { "\($0).\(shape)" }
.collect()
.assign(to: &$dataToView)
}
}
www.bigmountainstudio.com
}
282
Combine Mastery in SwiftUI
Collect By Count
04
[
] [
] [
] [
]
You can pass a number into the collect operator and it will keep collecting items and putting them into an array until it reaches that number and then it will publish
the array. It will continue to do this until the pipeline finishes.
􀎷
Operators
Collect By Count - View
struct Collect_ByCount: View {
@StateObject private var vm = Collect_ByCountViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Collect",
subtitle: "By Count",
desc: "You can collect a number of values you specify and put them into
arrays before publishing downstream.")
Text("Team Size: \(Int(vm.teamSize))")
Slider(value: $vm.teamSize, in: 2...4, step: 1,
minimumValueLabel: Text("2"),
maximumValueLabel: Text("4"), label:{ })
.padding(.horizontal)
Text("Teams")
List(vm.teams, id: \.self) { team in
Text(team.joined(separator: ", "))
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
The joined function puts all the items
in an array into a single string,
separated by the string you specify.
284
I’m using the collect operator to form
teams of two, which is actually an
array with two items.
When the slider changes value, I’m
using another pipeline to trigger the
recreation of this data into teams of 3
and 4.
Combine Mastery in SwiftUI
Operators
Collect By Count - View Model
class Collect_ByCountViewModel: ObservableObject {
@Published var teamSize = 2.0
@Published var teams: [[String]] = []
private var players: [String] = []
private var cancellables: Set<AnyCancellable> = []
A reference to the teamSize pipeline is stored
in cancellables. So why isn’t the players
pipeline in the createTeams function stored
too?
init() {
$teamSize
.sink { [unowned self] in createTeams(with: Int($0)) }
.store(in: &cancellables)
}
You need to keep the teamSize pipeline alive
because it’s actively connected to a slider on
the view.
func fetch() {
players = ["Mattie", "Chelsea", "Morgan", "Chase", "Kristin", "Beth", "Alex", "Ivan",
"Hugo", "Rod", "Lila", "Chris"]
But you don’t need to store a reference to the
players pipeline because you use it one time
and then you are done.
createTeams(with: Int(teamSize))
}
func createTeams(with size: Int) {
teams.removeAll()
_ = players.publisher
.collect(size)
.sink { [unowned self] (team) in
teams.append(team)
}
All of the player names will go through this pipeline
and be group together (or collected) into arrays using
the collect operator.
}
}
www.bigmountainstudio.com
285
Combine Mastery in SwiftUI
Collect By Time
[
] [
] [
] [
]
You can set a time interval for the collect operator. During that interval, the collect operator will be adding items coming down the pipeline to an array. When
the time interval is reached, the array is then published and the interval timer starts again.
Operators
Collect By Time - View
struct Collect_ByTime: View {
@StateObject private var vm = Collect_ByTimeViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Collect",
subtitle: "By Time",
desc: "Collect items within a certain amount of time, put them into an
array, and publish them with the collect by time operator.")
.layoutPriority(1)
Text(String(format: "Time Interval: %.1f seconds", vm.timeInterval))
Slider(value: $vm.timeInterval, in: 0.1...1,
Use width: 214
minimumValueLabel: Image(systemName: "hare"),
maximumValueLabel: Image(systemName: "tortoise"),
label: { Text("Interval") })
.padding(.horizontal)
Text("Collections")
List(vm.collections, id: \.self) { items in
Text(items.joined(separator: " "))
}
I have a Timer publisher that is publishing every 0.1 seconds.
}
Every time something is published, I send a 🟢 down the
.font(.title)
pipeline instead. These are collected into an array every 0.7
seconds and then published.
}
}
www.bigmountainstudio.com
287
Combine Mastery in SwiftUI
Operators
Collect By Time - View Model
class Collect_ByTimeViewModel: ObservableObject {
@Published var timeInterval = 0.5
@Published var collections: [[String]] = []
private var cancellables: Set<AnyCancellable> = []
private var timerCancellable: AnyCancellable?
Since the collect operator
publishes arrays, I created an
array of arrays type to hold
everything published.
init() {
$timeInterval
Every time timeInterval changes
(slider moves), call fetch().
.sink { [unowned self] _ in fetch() }
.store(in: &cancellables)
}
func fetch() {
collections.removeAll()
timerCancellable?.cancel()
Since the fetch function will get called repeatedly
as the slider is moving, I’m canceling the pipeline
so it starts all over again.
Use width: 214
timerCancellable = Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
Replace anything that comes down
the pipeline with a 🟢 .
.map { _ in "🟢 " }
.collect(.byTime(RunLoop.main, .seconds(timeInterval)))
.sink{ [unowned self] (collection) in
collections.append(collection)
}
You can also use milliseconds, microseconds, etc.
}
}
RunLoop.main is basically a mechanism to specify where and how work is done. I’m specifying I want
work done on the main thread. You could also use: DispatchQueue.main or OperationQueue.main
www.bigmountainstudio.com
288
Combine Mastery in SwiftUI
Collect By Time Or Count
04
[
]
[
] [
] [
]
When using collect you can also set it with a time interval and a count. When one of these limits is reached, the items collected will be published.
Operators
Collect by Time or Count - View
struct Collect_ByTimeOrCount: View {
@StateObject private var vm = Collect_ByTimeOrCountViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Collect",
subtitle: "By Time Or Count",
desc: "You can collect items and publish them when a certain time limit
is hit or when a count is reached.")
.layoutPriority(1)
Text("Count: 4")
Text("Time Interval: 1 second")
Use width: 214
Text("Collections")
.bold()
List(vm.collections, id: \.self) { items in
Text(items.joined(separator: " "))
}
}
.font(.title)
From what I can see from
experimentation, it seems to
publish when both the count
and interval are reached.
When you look at the
screenshot, it is publishing every
4 items AND, after one second, it
publishes whatever is remaining.
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
The joined function puts all the items
in an array into a single string,
separated by the string you specify.
290
I could be wrong on this but I
couldn’t find any good
documentation that breaks this
down clearly.
Combine Mastery in SwiftUI
Operators
Collect by Time or Count - View Model
class Collect_ByTimeOrCountViewModel: ObservableObject {
@Published var collections: [[String]] = []
private var timerCancellable: AnyCancellable?
func fetch() {
collections.removeAll()
timerCancellable?.cancel()
RunLoop.main is basically
a mechanism to specify
where and how work is
done. I’m specifying I want
work done on the main
thread. You could also use:
DispatchQueue.main
OperationQueue.main
timerCancellable = Timer
The delay can be specified in
many different ways such as:
.seconds
.milliseconds
.publish(every: 0.1, on: .main, in: .common)
.microseconds
.autoconnect()
.nanoseconds
.map { _ in "🟢 " }
.collect(.byTimeOrCount(RunLoop.main, .seconds(1), 4))
.sink{ [unowned self] (collection) in
collections.append(collection)
}
}
This is where you specify the count.
}
www.bigmountainstudio.com
291
Combine Mastery in SwiftUI
IgnoreOutput
This operator is pretty straightforward in its purpose. Anything that comes down the pipeline will be ignored and will never reach a subscriber. A sink subscriber will
still detect when it is finished or if it has failed though.
Operators
IgnoreOutput - View
struct IgnoreOutput_Intro: View {
@StateObject private var vm = IgnoreOutput_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("IgnoreOutput",
subtitle: "Introduction",
desc: "As the name suggests, the ignoreOutput operator ignores all items
coming down the pipeline but you can still tell if the pipeline
finishes or fails.")
.layoutPriority(1)
Use width: 214
List(vm.dataToView, id: \.self) { item in
Text(item)
}
These two List views are
actually using the same
publisher.
Text("Ignore Output:")
.bold()
The only difference is the
second pipeline is using
the ignoreOutput
operator.
List(vm.dataToView2, id: \.self) { item in
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
293
Combine Mastery in SwiftUI
Operators
IgnoreOutput - View Model
class IgnoreOutput_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var dataToView2: [String] = []
func fetch() {
let dataIn = ["Value 1", "Value 2", "Value 3"]
_ = dataIn.publisher
.sink { [unowned self] (item) in
dataToView.append(item)
}
Use width: 214
As you can see, all the
values never made it
through the pipeline
because they were
ignored.
_ = dataIn.publisher
.ignoreOutput()
.sink(receiveCompletion: { [unowned self] completion in
dataToView2.append("Pipeline Finished")
}, receiveValue: { [unowned self] _ in
You also can see the
receiveValue closure was
never run either but the
receiveCompletion was.
dataToView2.append("You should not see this.")
})
}
}
www.bigmountainstudio.com
294
Combine Mastery in SwiftUI
Reduce
The reduce operator gives you a closure to examine not only the current item coming down the pipeline but also the previous item that was returned from the
reduce closure. After the pipeline finishes, the reduce function will publish the last item remaining.
If you’re familiar with the scan operator you will notice the functions look nearly identical. The main difference is that reduce will only publish one item at the end.
Operators
Reduce - View
struct Reduce_Intro: View {
@StateObject private var vm = Reduce_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Reduce",
subtitle: "Introduction",
desc: "The reduce operator provides a closure for you to examine all
items BEFORE publishing one final value when the pipeline
finishes.")
List(vm.animals, id: \.self) { animal in
Text(animal)
Use width: 214
}
Text("Longest animal name: ")
+ Text("\(vm.longestAnimalName)")
.bold()
}
.font(.title)
.onAppear {
vm.fetch()
}
}
In this example, the reduce operator is being used to evaluate
all of the items to find the animal with the longest name.
}
www.bigmountainstudio.com
296
Combine Mastery in SwiftUI
Operators
Reduce - View Model
class Reduce_IntroViewModel: ObservableObject {
@Published var longestAnimalName = ""
@Published var animals: [String] = []
func fetch() {
let dataIn = ["elephant", "deer", "mouse", "hippopotamus", "rabbit", "aardvark"]
If you’re familiar with the scan operator then this
operator signature might look a little familiar.
_ = dataIn.publisher
.sink { [unowned self] (item) in
The first parameter is a default value so the first item
has something it can be compared to or examined in
some way.
animals.append(item)
}
dataIn.publisher
The closure’s input parameter named
longestNameSoFar is actually the previous item that
was returned from the reduce operator.
The nextName is the current item.
.reduce("") { (longestNameSoFar, nextName) in
if nextName.count > longestNameSoFar.count {
return nextName
}
return longestNameSoFar
}
.assign(to: &$longestAnimalName)
}
Shorthand Argument Names
Note: An even shorter way to write this is to use
shorthand argument names like this:
}
.reduce("") { $0.count > $1.count ? $0 : $1 }
www.bigmountainstudio.com
297
Combine Mastery in SwiftUI
TryReduce
error
The tryReduce will only publish one item, just like reduce will, but you also have the option to throw an error. Once an error is thrown, the pipeline will then finish.
Any try operator marks the downstream pipeline as being able to fail which means that you will have to handle potential errors in some way.
Operators
TryReduce - View
struct TryReduce: View {
@StateObject private var vm = TryReduceViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryReduce",
subtitle: "Introduction",
desc: "The tryReduce works just like reduce except it also allows you to
throw an error. When an error is thrown, the pipeline fails and is finished.")
List(vm.animals, id: \.self) { animal in
Text(animal)
}
Use width: 214
Text("Longest animal name: ")
+ Text("\(vm.longestAnimalName)")
.bold()
This alert monitors a published property
on the view model so once it becomes
not nil it will present an alert.
}
.font(.title)
.onAppear {
vm.fetch()
}
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}
www.bigmountainstudio.com
299
Combine Mastery in SwiftUI
Operators
TryReduce - View Model
class TryReduceViewModel: ObservableObject {
@Published var longestAnimalName = ""
@Published var animals: [String] = []
@Published var error: NotAnAnimalError?
func fetch() {
let dataIn = ["elephant", "deer", "mouse", "oak tree", "hippopotamus", "rabbit", "aardvark"]
_ = dataIn.publisher
.sink { [unowned self] (item) in
animals.append(item)
}
An error is thrown when something with the word “tree” is found. The
error is conforming to Identifiable so it can be monitored with an
alert modifier on the view:
_ = dataIn.publisher
.tryReduce("") { (longestNameSoFar, nextName) in
if nextName.contains("tree") {
throw NotAnAnimalError()
}
struct NotAnAnimalError: Error, Identifiable {
let id = UUID()
let message = "We found an item that was not an animal."
}
if nextName.count > longestNameSoFar.count {
return nextName
}
return longestNameSoFar
}
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = error as? NotAnAnimalError
}
} receiveValue: { [unowned self] longestName in
longestAnimalName = longestName
}
When using a try operator the pipeline recognizes that it can now fail. So
a sink with just receiveValue will not work. The error should be handled
in some way so the sink’s completion will assign it to a published property
to be shown on the view.
}
}
www.bigmountainstudio.com
300
Combine Mastery in SwiftUI
SELECTING SPECIFIC
ELEMENTS
First
The first operator is pretty simple. It will publish the first element that comes through the pipeline and then turn off (finish) the pipeline.
Operators
First - View
struct First_Intro: View {
@StateObject private var vm = First_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("First",
subtitle: "Introduction",
desc: "The first operator will return the very first item and then finish
the pipeline.")
Text("The first guest will be:")
Text(vm.firstGuest)
.bold()
Form {
Use width: 214
Section(header: Text("Guest List").font(.title2).padding()) {
ForEach(vm.guestList, id: \.self) { guest in
Text(guest)
}
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
303
Combine Mastery in SwiftUI
Operators
First - View Model
class First_IntroViewModel: ObservableObject {
@Published var firstGuest = ""
@Published var guestList: [String] = []
func fetch() {
let dataIn = ["Jordan", "Chase", "Kaya", "Shai", "Novall", "Sarun"]
_ = dataIn.publisher
.sink { [unowned self] (item) in
guestList.append(item)
}
dataIn.publisher
.first()
.assign(to: &$firstGuest)
The first operator will just return one item. Since the
pipeline will finish right after that, we can use the
assign(to:) subscriber and set the published property.
}
}
www.bigmountainstudio.com
304
Combine Mastery in SwiftUI
First(where:)
==
The first(where:) operator will evaluate items coming through the pipeline and see if they satisfy some condition in which you set. The first item that satisfies
your condition will be the one that gets published and then the pipeline will finish.
􀎷
Operators
First(where:) - View
struct First_Where: View {
@StateObject private var vm = First_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("First",
subtitle: "Where",
desc: "The first(where:) operator is used to publish the first item that
satisfies a condition you set and then finish the pipeline.")
.layoutPriority(1)
TextField("search criteria", text: $vm.criteria)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
The criteria property
changing is what triggers
the search.
Text("First Found: ") + Text(vm.firstFound).bold()
Form {
List(vm.deviceList, id: \.self) { device in
Text(device)
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
The idea here is to use the first(where:)
operator to find the first device that matches
the user’s search criteria.
}
}
www.bigmountainstudio.com
306
Combine Mastery in SwiftUI
Operators
First(where:) - View Model
class First_WhereViewModel: ObservableObject {
@Published var firstFound = ""
@Published var deviceList: [String] = []
@Published var criteria = ""
private var criteriaCancellable: AnyCancellable?
init() {
criteriaCancellable = $criteria
.sink { [unowned self] searchCriteria in
findFirst(criteria: searchCriteria)
}
}
The dollar sign ($) is used to access the criteria’s
publisher. Every time the criteria changes, its value
is sent through the pipeline.
Note: You could probably improve this pipeline
with some additional operators such as debounce
and removeDuplicates.
func fetch() {
deviceList = ["iPhone 4", "iPhone 15", "iPad Pro (14-inch)", "MacBook Pro 20-inch"]
}
func findFirst(criteria: String) {
deviceList.publisher
.first { device in
When the first device is found to match the criteria, it’ll be assigned to the
firstFound and the pipeline will finish.
If nothing is found then the replaceEmpty operator will return “Nothing found”.
device.contains(criteria)
}
.replaceEmpty(with: "Nothing found")
.assign(to: &$firstFound)
}
Shorthand Argument Names
Note: An even shorter way to write this is to use
shorthand argument names like this:
}
.first { $0.contains(criteria) }
www.bigmountainstudio.com
307
Combine Mastery in SwiftUI
TryFirst(where:)
error
==
The tryFirst(where:) operator works just like first(where:) except it also has the ability to throw errors from the provided closure. If an error is thrown, the
pipeline closes and finishes.
Any try operator marks the downstream pipeline as being able to fail which means that you will have to handle potential errors in some way.
Operators
TryFirst(where:) - View
struct TryFirst_Where: View {
@StateObject private var vm = TryFirst_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryFirst",
subtitle: "Where",
desc: "Use tryFind(where: ) when you need to be able to throw an error in
the pipeline.")
.layoutPriority(1)
TextField("search criteria", text: $vm.criteria)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text("First Found: ") + Text(vm.firstFound).bold()
Use width: 214
Form {
List(vm.deviceList, id: \.self) { device in
Text(device)
}
}
If an error is assigned to the view model’s
}
error property, this alert modifier will
.font(.title)
detect it and present an Alert.
.onAppear {
vm.fetch()
}
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}
www.bigmountainstudio.com
309
Combine Mastery in SwiftUI
Operators
TryFirst(where:) - View Model
class TryFirst_WhereViewModel: ObservableObject {
@Published var firstFound = ""
@Published var deviceList = ["iPhone 4", "iPhone 15", "Google Pixel", "iPad Pro (14-inch)", "MacBook Pro 20-inch"]
@Published var criteria = ""
@Published var error: InvalidDeviceError?
private var cancellables: Set<AnyCancellable> = []
init() {
$criteria
.dropFirst()
.debounce(for: 0.5, scheduler: RunLoop.main)
.sink { [unowned self] searchCriteria in
findFirst(criteria: searchCriteria)
}
.store(in: &cancellables)
}
func findFirst(criteria: String) {
deviceList.publisher
.tryFirst { device in
if device.contains("Google") {
throw InvalidDeviceError()
}
return device.contains(criteria)
}
.replaceEmpty(with: "Nothing found")
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = error as? InvalidDeviceError
}
} receiveValue: { [unowned self] foundDevice in
firstFound = foundDevice
}
.store(in: &cancellables)
}
In this example, we are going to throw an error and assign it to the error
published property so the view can get notified. The error conforms to
Identifiable so the alert modifier on the view can use it:
struct InvalidDeviceError: Error, Identifiable {
let id = UUID()
let message = "Whoah, what is this? We found a non-Apple device!"
}
Learn More
• dropFirst
• debounce
• replaceEmpty
}
www.bigmountainstudio.com
310
Combine Mastery in SwiftUI
Last
Use the last operator when you want to know what the last item is that comes down a pipeline.
Operators
Last - View
struct Last_Intro: View {
@StateObject private var vm = Last_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Last",
subtitle: "Introduction",
desc: "The last operator will give you the last item that came through
the pipeline when it finishes.")
Text("Your Destination:")
Text(vm.destination)
.bold()
Use width: 214
The last operator is being used to get the last city
in the user’s list of destinations.
Form {
Section(header: Text("Itinerary").font(.title2).padding()) {
ForEach(vm.itinerary, id: \.self) { city in
Text(city)
}
}
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
312
Combine Mastery in SwiftUI
Operators
Last - View Model
class Last_IntroViewModel: ObservableObject {
@Published var destination = ""
@Published var itinerary: [String] = []
func fetch() {
itinerary = ["Salt Lake City, UT", "Reno, NV", "Yellowstone, CA"]
itinerary.publisher
.last()
.replaceEmpty(with: "Enter a city")
.assign(to: &$destination)
The last operator will just return one item when the
pipeline finishes. Because of that, we can use the
assign(to:) subscriber and set the published property.
}
There are no try operators or anything else that can
throw an error so we don’t need a subscriber for
handling pipeline failures.
}
www.bigmountainstudio.com
313
Combine Mastery in SwiftUI
Last(where:)
==
This operator will find the last item that came through a pipeline that satisfies the criteria you provided. The last item will only be published once the pipeline has
finished. There may be many items that satisfy your criteria but only the last one is published.
Operators
Last(where:) - View
struct Last_Where: View {
@StateObject private var vm = Last_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Last",
subtitle: "Where",
desc: "Specify criteria for the last operator to give you the last item
that matches it when the pipeline finishes.")
Text("Last man on Earth:")
Use width: 214
Text(vm.lastMan)
.bold()
The view model has a pipeline that will use the
last operator to filter out all the men that are
on Earth and find the last one.
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
315
Combine Mastery in SwiftUI
Operators
Last(where:) - View Model
struct Alien {
var name = ""
var gender = ""
var planet = ""
}
class Last_WhereViewModel: ObservableObject {
@Published var lastMan = ""
@Published var dataToView: [String] = []
func fetch() {
let dataIn = [Alien(name: "Matt", gender: "man", planet: "Mars"),
Alien(name: "Alex", gender: "non-binary", planet: "Venus"),
Alien(name: "Rod", gender: "man", planet: "Earth"),
Alien(name: "Elaf", gender: "female", planet: "Mercury"),
Alien(name: “Max", gender: "non-binary", planet: "Jupiter"),
Alien(name: "Caleb", gender: "man", planet: "Earth"),
Alien(name: "Ellen", gender: "female", planet: "Venus")]
Specify criteria in the closure and after the pipeline finishes, the
last of whatever is remaining will be published.
dataIn.publisher
.last(where: { alien in
alien.gender == "man" && alien.planet == "Earth"
})
.map { $0.name }
.assign(to: &$lastMan)
}
Shorthand Argument Names
Let’s use map to republish
just the name.
Note: An even shorter way to write this is to use shorthand
argument names like this:
}
.last { $0.gender == "man" && $0.planet == "Earth" }
www.bigmountainstudio.com
316
Combine Mastery in SwiftUI
TryLast(where:)
error
==
The tryLast(where:) operator works just like last(where:) except it also has the ability to throw errors from within the closure provided. If an error is thrown,
the pipeline closes and finishes.
Any try operator marks the downstream pipeline as being able to fail which means that you will have to handle potential errors in some way.
Operators
LastTry(where:) - View
struct TryLast_Where: View {
@StateObject private var vm = TryLast_WhereViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryLast",
subtitle: "Where",
desc: "Specify criteria for the last operator to give you the last item
that matches it when the pipeline finishes or throw an error.")
Text("Last man on Earth:")
Text(vm.lastMan)
.bold()
Form {
ForEach(vm.aliens, id: \.name) { alien in
HStack {
Text(alien.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(alien.planet)
.foregroundColor(.gray)
}
If an error is assigned to the view model’s
}
error property, this alert modifier will
}
Use width: 214
detect it and present an Alert.
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
318
Combine Mastery in SwiftUI
Operators
LastTry(where:) - View Model
class TryLast_WhereViewModel: ObservableObject {
@Published var lastMan = ""
@Published var aliens: [Alien] = []
@Published var error: InvalidPlanetError?
func fetch() {
aliens = [Alien(name:
Alien(name:
Alien(name:
Alien(name:
Alien(name:
Alien(name:
Alien(name:
"Rick", gender: "man", planet: "Mars"),
"Alex", gender: "non-binary", planet: "Venus"),
"Rod", gender: "man", planet: "Earth"),
"Elaf", gender: "female", planet: "Mercury"),
"Morty", gender: "man", planet: "Earth"),
"Ellen", gender: "female", planet: "Venus"),
"Flippy", gender: "non-binary", planet: "Pluto")]
_ = aliens.publisher
.tryLast(where: { alien in
if alien.planet == "Pluto" {
throw InvalidPlanetError()
}
In this example, we are going to throw an error and assign it to the error
published property so the view can get notified. The error conforms to
Identifiable so the alert modifier on the view can use it:
return alien.gender == "man" && alien.planet == "Earth"
})
.map { $0.name }
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = error as? InvalidPlanetError
}
} receiveValue: { [unowned self] lastEarthMan in
lastMan = lastEarthMan
}
struct InvalidPlanetError: Error, Identifiable {
let id = UUID()
let description = "Pluto is not a planet. Get out of here!"
}
}
}
www.bigmountainstudio.com
319
Combine Mastery in SwiftUI
Output(at:)
2
7
6
5
4
3
2
1
0
With the output(at:) operator, you can specify an index and when an item at that index comes through the pipeline it will be republished and the pipeline will finish. If
you specify a number higher than the number of items that come through the pipeline before it finishes, then nothing is published. (You won’t get any index out-ofbounds errors.)
􀎷
Operators
Output(at: ) - View
struct Output_At: View {
@StateObject private var vm = Output_AtViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Output(at: )",
subtitle: "Introduction",
desc: "Specify an index for the output operator and it will publish the
item at that position.")
Stepper("Index: \(vm.index)", value: $vm.index)
.padding(.horizontal)
Text("Animal: \(vm.selection)")
.italic()
.font(.title3)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
The Stepper is bound to
the index property which
will call a function to get
the animal at that index
using the output(at:)
operator.
.padding(.horizontal)
Text("Smart Animals")
.bold()
List(vm.animals, id: \.self) { animal in
Text(animal)
}
}
.font(.title)
}
}
www.bigmountainstudio.com
321
Combine Mastery in SwiftUI
Operators
Output(at: ) - View Model
class Output_AtViewModel: ObservableObject {
@Published var index = 0
@Published var selection = ""
@Published var animals = ["Chimpanzee", "Elephant", "Parrot", "Dolphin", "Pig", "Octopus"]
private var cancellable: AnyCancellable?
init() {
When the stepper on the view changes the
index property, we want to call getAnimal
using the new property.
cancellable = $index
.sink { [unowned self] in getAnimal(at: $0)}
}
func getAnimal(at index: Int) {
animals.publisher
.output(at: index)
.assign(to: &$selection)
Once the right item at the index is found, the
pipeline finishes and sets the value to the
published property.
}
}
www.bigmountainstudio.com
322
Combine Mastery in SwiftUI
Output(in:)
2…4
8
7
6
5
4
3
2
1
0
You can also use the output operator to select a range of values that come through the pipeline. This operator says, “I will only republish items that match the index
between this beginning number and this ending number.”
􀎷
Operators
Output(in:) - View
struct Output_In: View {
@StateObject private var vm = Output_InViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Output(in: )",
subtitle: "Introduction",
desc: "Use output(in:) operator to have your pipeline narrow down its
output with an index range.")
Stepper("Start Index: \(vm.startIndex)", value: $vm.startIndex)
.padding(.horizontal)
Stepper("End Index: \(vm.endIndex)", value: $vm.endIndex)
.padding(.horizontal)
List(vm.animals) { animal in
Text("\(animal.index): \(animal.name)")
}
Increasing and decreasing the
steppers will narrow down the
items in the list using the
output(in:) operator.
}
.font(.title)
}
}
www.bigmountainstudio.com
324
Combine Mastery in SwiftUI
Operators
Output(in:) - View Model
class Output_InViewModel: ObservableObject {
The Animal struct conforms to Identifiable so it can be iterated
though on the UI:
@Published var startIndex = 0
@Published var endIndex = 5
@Published var animals: [Animal] = []
var cancellables: Set<AnyCancellable> = []
let cache = [Animal(index: 0, name: "Chimpanzee"),
struct Animal: Identifiable {
let id = UUID()
var index = 0
var name = ""
}
Animal(index: 1, name: "Elephant"),
Animal(index: 2, name: "Parrot"),
Animal(index: 3, name: "Dolphin"),
Animal(index: 4, name: "Pig"),
Animal(index: 5, name: "Octopus")]
init() {
$startIndex
.map { [unowned self] index in
if index < 0 {
return 0
} else if index > endIndex {
return endIndex
}
Unlike the output(at:) operator which returns one item at an index,
the output(in:) operator will crash your app if the index goes out
of bounds. So you will have to make sure the start index does not
go below zero or become greater than the end index.
(Note: You could also control this on the UI or with other methods.)
return index
}
.sink { [unowned self] index in
getAnimals(between: index, end: endIndex)
}
.store(in: &cancellables)
www.bigmountainstudio.com
325
Combine Mastery in SwiftUI
Operators
$endIndex
If the end index becomes less than the start index, the app will crash. But
if the end index becomes greater than the number of items that come
through the pipeline you are safe.
(Note: You could also control this on the UI or with other methods.)
.map { [unowned self] index in
index < startIndex ? startIndex : index
}
.sink { [unowned self] index in
getAnimals(between: startIndex, end: index)
}
.store(in: &cancellables)
}
func getAnimals(between start: Int, end: Int) {
animals.removeAll()
You can, of course, just hard-code the range.
cache.publisher
.output(in: start...end)
.sink { [unowned self] animal in
animals.append(animal)
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
326
Combine Mastery in SwiftUI
SPECIFYING SCHEDULERS
Operators
Concept of Foreground and Background Work
I could assume you already know this but I want to cover this concept of foreground and background work really quick.
Foreground Work
Background Work
The computer gets worked on in the background so that the
employees in the foreground can keep doing their jobs greeting and talking to customers. When the computer is
ready, it gets sent to the foreground.
At a store, the employees will greet and talk to customers. If the customer has
a computer that needs work done, many times it will get sent to the
background to get looked at and fixed.
www.bigmountainstudio.com
328
Combine Mastery in SwiftUI
Operators
Foreground and Background Work in iOS
An app works a lot like the store example on the previous page. The UI is the part that does the foreground work while other work can be done in the
background so the foreground can still do its job and talk to the user.
Foreground Work
Background Work
The UI of your app handles the foreground work. The user taps that button to
get data from the internet, it could take a while so you send it to the
background to go get the data. (This is usually called the “main thread”.)
www.bigmountainstudio.com
In the background, work that might take longer is performed
so the UI can keep doing its job and talking to the user. When
the image is fetched, it sends it back to the foreground.
329
Combine Mastery in SwiftUI
Operators
Foreground and Background Work on a Pipeline
There are two ways you can control where work is done on a pipeline. With a subscribe or receive operator.
OK, we got some data from
the background. Let’s move it to the
foreground (main) thread and send
it downstream.
Hey, you publishers and
operators upstream, I want you to do
your work in the background.
subscribe
www.bigmountainstudio.com
330
receive
Combine Mastery in SwiftUI
Receive(on:)
receive
background
foreground (main)
(
,
)
Sometimes publishers will be doing work in the background. If you then try to display the data on the view it may or may not be displayed. Xcode will also show you
the “purple warning” which is your hint that you need to move data from the background to the foreground (or main thread) so it can be displayed.
Operators
Receive(on:) - View
struct Receive_Intro: View {
@StateObject private var vm = Receive_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Receive",
subtitle: "Introduction",
desc: "The receive operator will move items coming down the pipeline to
another pipeline (thread).")
Button("Get Data From The Internet") {
Use width: 214
vm.fetch()
}
vm.imageView
.resizable()
.scaledToFit()
Spacer(minLength: 0)
}
.font(.title)
}
}
www.bigmountainstudio.com
332
In this example, a URL is used
to retrieve an image on a
background thread, and then
it is moved to a foreground
(main) thread to be displayed
on the UI.
Combine Mastery in SwiftUI
Operators
Receive(on:) - View Model
class Receive_IntroViewModel: ObservableObject {
@Published var imageView = Image("blank.image")
@Published var errorForAlert: ErrorForAlert?
var cancellables: Set<AnyCancellable> = []
func fetch() {
let url = URL(string: "https://http.cat/401")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.tryMap { data in
guard let uiImage = UIImage(data: data) else {
throw ErrorForAlert(message: "Did not receive a valid image.")
The dataTaskPublisher will automatically do
}
work in the background. If you set a
return Image(uiImage: uiImage)
breakpoint, you can see in the Debug
}
navigator that it’s not on the main thread.
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
if case .failure(let error) = completion {
if error is ErrorForAlert {
errorForAlert = (error as! ErrorForAlert)
} else {
The RunLoop is a scheduler which
errorForAlert = ErrorForAlert(message: "Details: \(error.localizedDescription)")
is basically a mechanism to specify
}
where and how work is done. I’m
}
specifying I want work done on the
}, receiveValue: { [unowned self] image in
imageView = image
main thread. You could also use
})
these other schedulers:
RunLoop
.store(in: &cancellables)
Run loops manage events and
work. It allows multiple things to
happen simultaneously.
}
}
www.bigmountainstudio.com
333
DispatchQueue.main
OperationQueue.main
Combine Mastery in SwiftUI
Operators
How do I know if I should use receive(on:)?
Here are some things you can look for.
1. Purple warning in Xcode status bar
3. Message in Issue navigator
2. Purple warning in Xcode editor
4. Message in debug console
When you see these things, you know it is time to use receive(on:).
www.bigmountainstudio.com
334
Combine Mastery in SwiftUI
Subscribe(on:)
subscribe
receive
background
main
Use the subscribe(on:) operator when you want to suggest that work be done in the background for upstream publishers and operators. I say “suggest” because
subscribe(on:) does NOT guarantee that the work in operators will actually be performed in the background. Instead, it affects the thread where publishers get
their subscriptions (from the subscriber/sink), where they receive the request for how much data is wanted, where they receive the data, where they get cancel
requests from, and the thread where the completion event happens. (Apple calls these 5 events “operations”.)
I will show you in more detail how you can see this happening in the following pages.
Operators
Subscribe(on:) - View
struct Subscribe_Intro: View {
@StateObject private var vm = Subscribe_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Subscribe",
subtitle: "Introduction",
desc: "The subscribe operator will schedule operations to be done in the
background for all upstream publishers and operators.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
When I say “operations”, I specifically mean these 5 events for publishers:
1. Receive Subscription - This is when a subscriber, like sink or assign,
says, “Hey, I would like some data now.”
2. Receive Output - This is when an item is coming through the pipeline
and this publisher/operator receives it.
3. Receive Completion - When the pipeline completes, this event occurs.
4. Receive Cancel - Early in this book, you learned to create a cancellable
pipeline. This happens when a pipeline is cancelled.
5. Receive Request - This is where the subscriber says how much data it
requests (also called “demand”). It is usually either “unlimited” or
“none”.
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
336
Combine Mastery in SwiftUI
Operators
Subscribe(on:) - View Model
class Subscribe_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Which", "thread", "is", "used?"]
_ = dataIn.publisher
.map { item in
print("map: Main thread? \(Thread.isMainThread)")
return item
}
.handleEvents(receiveSubscription: { subscription in
print("receiveSubscription: Main thread? \(Thread.isMainThread)")
}, receiveOutput: { item in
print("\(item) - receiveOutput: Main thread? \(Thread.isMainThread)")
}, receiveCompletion: { completion in
print("receiveCompletion: Main thread? \(Thread.isMainThread)")
}, receiveCancel: {
print("receiveCancel: Main thread? \(Thread.isMainThread)")
}, receiveRequest: { demand in
print("receiveRequest: Main thread? \(Thread.isMainThread)")
})
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink { [unowned self] item in
dataToView.append(item)
}
}
}
Even though subscribe(on:) is added to the pipeline, the
map operator still performs on the main thread. So you can
see that this operator does NOT guarantee that work in
operators will be performed in the background.
But the 5 operations all perform in the background.
www.bigmountainstudio.com
337
The handleEvents operator
is a great way to demonstrate
and show where the 4
operations are doing their
work.
Learn More
Learn more about
handleEvents in the
Debugging chapter.
nd
rou
g
k
bac
All
Combine Mastery in SwiftUI
SUBSCRIBERS
Assign(to:)
Data
@Published
Property
The assign(to:) subscriber receives values and directly assigns the value to a @Published property. This is a special subscriber that works with published
properties. In a SwiftUI app, this is a very common subscriber.
Assign(to:)
View
struct AssignTo_Intro: View {
@StateObject private var vm = AssignToViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Assign To",
subtitle: "Introduction",
desc: "The assign(to:) subscriber is very specific to JUST @Published
properties. It will easily allow you to add the value that come
down the pipeline to your published properties which will then
notify and update your views.")
Use width: 214
Text(vm.greeting)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
340
Combine Mastery in SwiftUI
Assign(to:)
View Model
class AssignToViewModel: ObservableObject {
@Published var name = ""
@Published var greeting = ""
Pipeline: Whenever the name changes, the greeting is automatically
updated.
init() {
$name
.map { [unowned self] name in
createGreeting(with: name)
}
.assign(to: &$greeting)
}
No AnyCancellable
Notice you don’t have to keep a reference to an AnyCancellable type.
This is because Combine will automatically handle this for you.
func fetch() {
name = "Developer"
}
This feature is exclusive to just this subscriber.
When this view model is de-initialized and then the @Published
properties de-initialize, the pipeline will automatically be canceled.
func createGreeting(with name: String) -> String {
let hour = Calendar.current.component(.hour, from: Date())
var prefix = ""
switch hour {
case 0..<12:
prefix = "Good morning, "
case 12..<18:
prefix = "Good afternoon, "
default:
prefix = "Good evening, "
}
return prefix + name
}
}
www.bigmountainstudio.com
341
Combine Mastery in SwiftUI
Sink
The sink subscriber will allow you to just receive values and do anything you want with them. There is also an option to run code when the pipeline completes,
whether it completed from an error or just naturally.
􀎷
Sink(receiveValue:)
Sink(receiveValue:) - View
struct Sink_Intro: View {
@StateObject private var vm = Sink_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Sink",
subtitle: "Introduction",
desc: "The sink subscriber allows you to access every value that comes
down the pipeline and do something with it.")
Button("Add Name") {
vm.fetchRandomName()
}
HStack {
Text("A to M")
.frame(maxWidth: .infinity)
Text("N to Z")
.frame(maxWidth: .infinity)
}
HStack {
List(vm.aToM, id: \.self) { name in
Text(name)
}
List(vm.nToZ, id: \.self) { name in
Text(name)
}
}
}
.font(.title)
}
}
www.bigmountainstudio.com
343
Combine Mastery in SwiftUI
Sink(receiveValue:)
Sink(receiveValue:) - View Model
class Sink_IntroViewModel: ObservableObject {
let names = ["Joe", "Nick", "Ramona", "Brad", "Mark", "Paul", "Sean", "Alice", "Kaya", "Emily"]
@Published var newName = ""
@Published var aToM: [String] = []
Pipeline: The idea here is when a new
value is assigned to newName, it is
examined and decided which array to
add it to.
@Published var nToZ: [String] = []
var cancellable: AnyCancellable?
init() {
cancellable = $newName
.dropFirst()
.sink { [unowned self] (name) in
The first value to come through is the empty
string the newName property is assigned.
We want to skip this by using the
dropFirst operator.
let firstLetter = name.prefix(1)
if firstLetter < "M" {
aToM.append(name)
} else {
nToZ.append(name)
}
If the value coming through the pipeline was
always assigned to the same @Published
property, you could use the assign(to:)
subscriber instead.
}
}
Note: There are two types of pipelines:
• Error-throwing
• Non-Error-Throwing
You can ONLY use
sink(receiveValue:) on non-errorthrowing pipelines.
Not sure which kind of pipeline you
have?
Don’t worry, Xcode won’t let you use this
subscriber on an error-throwing
pipeline.
func fetchRandomName() {
newName = names.randomElement()!
Learn more in the Handling Errors
chapter.
}
}
www.bigmountainstudio.com
344
Combine Mastery in SwiftUI
Sink(receiveCompletion: receiveValue:)
Sink Completion - View
struct Sink_Completion: View {
@StateObject private var vm = SinkCompletionViewModel()
var body: some View {
ZStack {
VStack(spacing: 20) {
HeaderView("Sink",
subtitle: "Receive Completion",
desc: "The sink subscriber also has a parameter for a closure that
will run when the pipeline completes publishing. One use might
be to know when to stop showing an activity indicator.”)
Button("Start Processing") { vm.fetch() }
Text(vm.data)
}
.font(.title)
if vm.isProcessing { ProcessingView() }
}
}
}
The goal here is to show the
ProcessingView while the
pipeline is working and then to
hide it when it’s finished.
struct ProcessingView: View {
var body: some View {
VStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(2)
.padding()
Text("Processing...")
.foregroundColor(.white)
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 15).fill(Color.black.opacity(0.9)))
}
}
www.bigmountainstudio.com
345
Combine Mastery in SwiftUI
Sink(receiveCompletion: receiveValue:)
Sink Completion - View Model
class SinkCompletionViewModel: ObservableObject {
@Published var data = ""
@Published var isProcessing = false
var cancellables: Set<AnyCancellable> = []
Pipeline: The idea here is when some operation
has started, show the progress indicator and when
the pipeline completes, turn it off.
func fetch() {
isProcessing = true
[1,2,3,4,5].publisher
.delay(for: 1, scheduler: RunLoop.main)
.sink { [unowned self] (completion) in
This will trigger showing the ProcessingView.
Add some extra time to this pipeline to
slow it down.
isProcessing = false
} receiveValue: { [unowned self] (value) in
data = data.appending(String(value))
When completed, this will hide the
ProcessingView.
Use width: 214
}
.store(in: &cancellables)
}
}
Learn More
• delay
• See another example of hiding/
showing the ProgressView using the
handleEvents operator
www.bigmountainstudio.com
346
Combine Mastery in SwiftUI
Sink(receiveCompletion: receiveValue:)
Sink Completion - Error - View
struct Sink_Completion_Error: View {
@StateObject private var vm = SinkCompletionErrorViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Sink",
subtitle: "Receive Completion - Error",
desc: "Sometimes your pipeline could have an error thrown that you want
to catch and show. You can check for errors in the sink subscriber
too.")
Button("Start Processing") {
Use width: 214
vm.fetch()
}
If this published property ever becomes
true then the error will show.
Text(vm.data)
}
.font(.title)
.alert(isPresented: $vm.showErrorAlert) {
Alert(title: Text("Error"), message: Text(vm.errorMessage))
}
}
}
www.bigmountainstudio.com
347
Combine Mastery in SwiftUI
Sink(receiveCompletion: receiveValue:)
Sink Completion - Error - View Model
struct NumberFiveError: Error {
}
class SinkCompletionErrorViewModel: ObservableObject {
@Published var data = ""
@Published var showErrorAlert = false
@Published var errorMessage = "Cannot process numbers greater than 5."
var cancellable: AnyCancellable?
func fetch() {
cancellable = [1,2,3,4,5].publisher
.tryMap { (value) -> String in
if value >= 5 {
throw NumberFiveError()
}
return String(value)
}
.sink { [unowned self] (completion) in
switch completion {
case .failure(_):
showErrorAlert.toggle()
case .finished:
print(completion)
}
data = String(data.dropLast(2))
} receiveValue: { [unowned self] (value) in
data = data.appending("\(value), ")
}
}
Pipeline: The idea here is to check values
coming through the pipeline and stop if
some condition is met.
Use width: 214
In this example, we’re examining the
completion input parameter to see if there
was a failure. If so, then we toggle an
indicator and show an alert on the view.
}
www.bigmountainstudio.com
348
Combine Mastery in SwiftUI
ORGANIZING
Using Properties & Functions
You don’t always have to assemble your whole pipeline in your observable object. You can store your publishers (with or without operators) in properties or return
publishers from functions to be used at a later time. Maybe you notice you have a common beginning to many of your pipelines. This is a good opportunity to extract
them out into a common property or function. Or maybe you are creating an API and you want to expose publishers to consumers.
Organizing
Using Properties & Functions - View
struct UsingProperties: View {
@StateObject private var vm = UsingPropertiesViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Using Properties",
subtitle: "Introduction",
desc: "You can store publishers in properties to be used later. The
publisher can also have operators connected to them too.")
Text("\(vm.lastName), \(vm.firstName)")
Text("Team")
Use width: 214
.bold()
List(vm.team, id: \.self) { name in
Text(name)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
All of the data on the UI comes from publishers stored in
properties or functions with subscribers attached to
them later.
}
}
www.bigmountainstudio.com
351
Combine Mastery in SwiftUI
Organizing
Using Properties & Functions - View Model
class UsingPropertiesViewModel: ObservableObject {
@Published var firstName = ""
@Published var lastName = ""
@Published var team: [String] = []
Here’s an example of just storing a publisher in a property.
var firstNamePublisher = Just("Mark")
var lastNameUppercased: Just<String> {
Just("Moeykens")
.map { $0.uppercased() }
}
If you’re adding operators, you might find it easier to use a closure. If there’s only one item
in a closure then you don’t need to use the get or the return keywords.
func teamPipeline(uppercased: Bool) -> AnyCancellable {
["Lisandro", "Denise", "Daniel"].publisher
.map {
uppercased ? $0.uppercased() : $0
}
.sink { [unowned self] name in
team.append(name)
}
}
func fetch() {
firstNamePublisher
.map { $0.uppercased() }
.assign(to: &$firstName)
You can also have functions that return whole pipelines. The sink
subscribers return AnyCancellable. The assign(to:) does not.
From here, you can just attach operators and
subscribers to your publisher properties.
lastNameUppercased
.assign(to: &$lastName)
_ = teamPipeline(uppercased: false)
}
}
www.bigmountainstudio.com
If you’re returning a whole pipeline, then just
call the function and handle the returned
cancellable in some way.
352
“Should I use a property
or a function?”
My own personal rule is I always
start with a property.
But then if the pipeline needs to use
a variable then I convert it to a
function and pass in the variable.
Combine Mastery in SwiftUI
AnyPublisher
The AnyPublisher object can represent, well, any publisher or operator. (Operators are a form of publishers.) When you create pipelines and want to store them in
properties or return them from functions, their resulting types can bet pretty big because you will find they are nested. You can use AnyPublisher to turn these
seemingly complex types into a simpler type.
Organizing
Pipeline Nesting
You can observe that when you add operators to your publisher, the types become nested.
Example Pipeline
The Type
Publishers.ReplaceError<
Publishers.Concatenate<
Publishers.Sequence<[String], Error>,
Publishers.ReceiveOn<
Publishers.Decode<
Publishers.Map<
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
URLSession.DataTaskPublisher,
JSONDecoder.Input>
.prepend("AWAY TEAM")
.replaceError(with: "No players found")
, String
, JSONDecoder>
, RunLoop>
>
>
Can you imagine returning this type from a function?
func publisher(url: URL) ->
Publishers.ReplaceError<Publishers.Concatenate<Publishers.Seque
nce<[String], Error>,
Publishers.ReceiveOn<Publishers.Decode<Publishers.Map<URLSessio
n.DataTaskPublisher, JSONDecoder.Input>, String, JSONDecoder>,
RunLoop>>> {
. . .
}
There’s a better way!
Instead, you can just return AnyPublisher. Yes, ONE type.
If you OPTION-Click on publisher, you can inspect the type.
www.bigmountainstudio.com
354
Combine Mastery in SwiftUI
Organizing
Using eraseToAnyPublisher
By using the operator eraseToAnyPublisher, you can simplify the return type of the publishing part of the pipeline (no subscriber).
Before
func publisher(url: URL) ->
Publishers.ReplaceError<Publishers.Concatenate<Publishers.S
equence<[String], Error>,
Publishers.ReceiveOn<Publishers.Decode<Publishers.Map<URLSe
ssion.DataTaskPublisher, JSONDecoder.Input>, String,
JSONDecoder>, RunLoop>>> {
return URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.prepend("AWAY TEAM")
.replaceError(with: "No players found")
}
This is a great solution for simplifying return types when using a
function.
After
func publisher(url: URL) -> AnyPublisher<String, Never> {
return URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.prepend("AWAY TEAM")
.replaceError(with: "No players found")
.eraseToAnyPublisher()
}
Add this operator to the end of your pipeline to simplify the return type.
Tip: If you’re not sure what the resulting type should be, then return a
simple type like String and then read the error message. It will tell you.
It also solves the problem when you have one function that can return
one or another pipeline. See the next pages for an example.
www.bigmountainstudio.com
355
Combine Mastery in SwiftUI
􀎷
Organizing
AnyPublisher - View
struct AnyPublisher_Intro: View {
@StateObject private var vm = AnyPublisher_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("AnyPublisher",
subtitle: "Introduction",
desc: "The AnyPublisher is a publisher that all publishers (and
operators) can become. You can use a special operator called
eraseToAnyPublisher to create this common object.")
.layoutPriority(1)
Toggle("Home Team", isOn: $vm.homeTeam)
.padding()
Text("Team")
.bold()
List(vm.team, id: \.self) { name in
Text(name)
}
}
The idea here is when you toggle the switch, a
different publisher is used to get a different team.
Both publishers are returned from the same
function. So the return types have to match.
.font(.title)
}
}
www.bigmountainstudio.com
356
Combine Mastery in SwiftUI
Organizing
AnyPublisher - View Model
class AnyPublisher_IntroViewModel: ObservableObject {
@Published var homeTeam = true
@Published var team: [String] = []
private var cancellables: Set<AnyCancellable> = []
init() {
$homeTeam
.sink { [unowned self] value in
fetch(homeTeam: value)
}
.store(in: &cancellables)
}
There is a pipeline on this toggle so
when the value changes, it re-fetches
the data to populate the list.
Use width: 214
func fetch(homeTeam: Bool) {
team.removeAll()
AppPublishers.teamPublisher(homeTeam: homeTeam)
.sink { [unowned self] item in
team.append(item)
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
AppPublishers.teamPublisher
returns a publisher that either gets the
home team or the away team.
These are two different pipelines that
can be returned from the same function
but use the same subscriber.
Let’s see how this is done on the next
page.
357
Combine Mastery in SwiftUI
Organizing
AppPublishers.teamPublisher
class AppPublishers {
static func teamPublisher(homeTeam: Bool) -> AnyPublisher<String, Never> {
1
if homeTeam {
return ["Stockton", "Malone", "Williams"].publisher
.prepend("HOME TEAM")
There may be a scenario in your app where you
need the same publisher on multiple views.
Instead of duplicating the publisher, you can
extract it to a common class like this.
.eraseToAnyPublisher()
} else {
let url = URL(string: "https://www.nba.com/api/getteam?id=21")!
2
I’m using hard-code values here for demonstration
purposes. But let’s suppose that these values are
cached for the app user’s home team.
return URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
1. Both of these publishers are returning strings
and never fail (meaning they don’t throw
errors).
2. This is a fake URL to get a team based on an id.
3. If you have read about dataTaskPublisher
then you know errors can be thrown. So to
make both pipelines return the same type of
AnyPublisher that never returns errors I use
the replaceError operator to intercept errors,
return a String and cancel the publisher.
data
}
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.prepend("AWAY TEAM")
.replaceError(with: "No players found")
3
.eraseToAnyPublisher()
}
}
Read more about this in the “Handling Errors”
chapter.
}
www.bigmountainstudio.com
358
Combine Mastery in SwiftUI
WORKING WITH MULTIPLE
PUBLISHERS
CombineLatest
Using the combineLastest operator you can connect two or more pipelines and then use a closure to process the latest data received from each publisher in some
way. There is also a combineLatest to connect 3 or even 4 pipelines together. You will still have just one pipeline after connecting all of the publishers.
Working with Multiple Publishers
CombineLatest - View
struct CombineLatest_Intro: View {
@StateObject private var vm = CombineLatest_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("CombineLatest",
subtitle: "Introduction",
desc: "You can combine multiple pipelines and pair up the last values
from each one and do something with them using the combineLatest
operator.")
VStack {
Image(vm.artData.artist)
.resizable()
.aspectRatio(contentMode: .fit)
Text(vm.artData.artist)
.font(.body)
}
.padding()
.background(vm.artData.color.opacity(0.3))
.padding()
Use width: 214
}
.font(.title)
.onAppear {
vm.fetch()
}
There are two publishers with many artists and many colors. But
the combineLatest is only interested in the LATEST (or sometimes
last) item each pipeline publishes.
The latest values from the two pipelines are joined together to
give us “Monet” and the color green.
}
}
www.bigmountainstudio.com
361
Combine Mastery in SwiftUI
Working with Multiple Publishers
CombineLatest - View Model
class CombineLatest_IntroViewModel: ObservableObject {
@Published var artData = ArtData()
let artists = ["Picasso", "Michelangelo", "van Gogh", "da Vinci", "Monet"]
The combineLatest receives the latest values from both
pipelines in the form of a Tuple.
The data is used to instantiate a new ArtData object and
sent down the pipeline.
let colors = [Color.red, Color.orange, Color.blue, Color.purple, Color.green]
struct ArtData: Identifiable {
func fetch() {
let id = UUID()
var artist = ""
_ = artists.publisher
var color = Color.clear
.combineLatest(colors.publisher) { (artist, color) in
var number = 0
}
return ArtData(artist: artist, color: color)
}
.sink { [unowned self] (artData) in
self.artData = artData
}
}
}
www.bigmountainstudio.com
By the way, I have photos in
the asset catalog that match
all the artists’ names.
362
Combine Mastery in SwiftUI
Working with Multiple Publishers
CombineLatest: More than 2 Publishers - View
struct CombineLatest_MoreThanTwo: View {
@StateObject private var vm = CombineLatest_MoreThanTwoViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("CombineLatest",
subtitle: "More Than Two",
desc: "If you're working with more than two publishers then you will have
to keep adding more input parameters into the closure.")
VStack {
Image(systemName: "\(vm.artData.number).circle")
Image(vm.artData.artist)
.resizable()
.aspectRatio(contentMode: .fit)
Text(vm.artData.artist)
.font(.body)
}
.padding()
.background(vm.artData.color.opacity(0.3))
.padding()
Use width: 214
}
.font(.title)
.onAppear {
vm.fetch()
}
A third publisher is included now and is providing the value for the
number at the top. This is simply the latest number from that
third publisher that is being matched up with the color and image
from the other two pipelines.
}
}
www.bigmountainstudio.com
363
Combine Mastery in SwiftUI
Working with Multiple Publishers
CombineLatest: More than 2 Publishers - View Model
class CombineLatest_MoreThanTwoViewModel: ObservableObject {
@Published var artData = ArtData(artist: "van Gogh", color: Color.red)
func fetch() {
let artists = ["Picasso", "Michelangelo"]
let colors = [Color.red, Color.purple, Color.blue, Color.orange]
let numbers = [1, 2, 3]
The three publishers used all have varying amounts of
data. But remember, the combineLatest is only
interested in the latest value the publisher sends down
the pipeline.
_ = artists.publisher
.combineLatest(colors.publisher, numbers.publisher) { (artist, color, number) in
return ArtData(artist: artist, color: color, number: number)
}
.sink { [unowned self] (artData) in
self.artData = artData
}
}
Notice the input parameters will keep increasing as you
add more publishers.
}
www.bigmountainstudio.com
364
Combine Mastery in SwiftUI
Working with Multiple Publishers
CombineLatest: Alternative
class CombineLatest_MoreThanTwoViewModel: ObservableObject {
@Published var artData = ArtData(artist: "van Gogh", color: Color.red)
func fetch() {
let artists = ["Picasso", "Michelangelo"]
let colors = [Color.red, Color.purple, Color.blue, Color.orange]
let numbers = [1, 2, 3]
_ = Publishers.CombineLatest3(artists.publisher, colors.publisher, numbers.publisher)
.map { (artist, color, number) in
return ArtData(artist: artist, color: color, number: number)
}
You can also use the
CombineLatest function directly
from the Publishers enum. There
are 3 different options:
CombineLatest for 2 publishers
CombineLatest3 for 3 publishers
CombineLatest4 for 4 publishers
.sink { [unowned self] (artData) in
self.artData = artData
}
}
}
www.bigmountainstudio.com
When using Publishers.CombineLatest, you will have to
include a map operator since there is no closure for code.
365
Combine Mastery in SwiftUI
FlatMap
You are used to seeing a value of some sort sent down a pipeline. But what if you wanted to use that value coming down the pipeline to retrieve more data from
another data source. You would essentially need a publisher within a publisher. The flatMap operator allows you to do this.
􀎷
Working with Multiple Publishers
FlatMap - View
struct FlatMap_Intro: View {
@StateObject private var vm = FlatMap_IntroViewModel()
@State private var count = 1
var body: some View {
VStack(spacing: 20) {
HeaderView("FlatMap",
subtitle: "Introduction",
desc: "The flatMap operator can be used to create a new publisher for
each item that comes through the pipeline.")
Text(vm.names.joined(separator: ", "))
Button("Find Gender Probability") {
vm.fetchNameResults()
}
List(vm.nameResults, id: \.name) { nameResult in
HStack {
Text(nameResult.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(nameResult.gender + ": ")
Text(getPercent(nameResult.probability))
}
}
}
In this example, an API
call is made with the
dataTaskPublisher for
each name that comes
down the pipeline.
func getPercent(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
return formatter.string(from: NSNumber(value: number)) ?? "N/A"
}
Notice the order of the
results does not match
the order of the names
above the button.
}
.font(.title)
}
www.bigmountainstudio.com
367
Combine Mastery in SwiftUI
Working with Multiple Publishers
FlatMap - View Model
struct NameResult: Decodable {
var name = ""
var gender = ""
var probability = 0.0
}
class FlatMap_IntroViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
@Published var nameResults: [NameResult] = []
The main publisher is the list of names. For each
name, a URL is created.
private var cancellables: Set<AnyCancellable> = []
That URL (and the original name coming down the
func fetchNameResults() {
pipeline) is passed into the flatMap operator’s
names.publisher
closure.
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
}
.flatMap { (name, url) -> AnyPublisher<NameResult, Never> in
The map here could be replaced with
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data } or .map(\.data).
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: NameResult.self, decoder: JSONDecoder())
If there is an error from either the
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher()
dataTaskPublisher or decode then I’m just
}
replacing it with a new NameResult object. This is
.receive(on: RunLoop.main)
why name is also passed into flatMap.
.sink { [unowned self] nameResult in
nameResults.append(nameResult)
}
.store(in: &cancellables)
Learn more about dataTaskPublisher here.
}
}
Learn more about replaceError here.
Learn more about eraseToAnyPublisher in the Organizing chapter.
www.bigmountainstudio.com
368
Combine Mastery in SwiftUI
Working with Multiple Publishers
FlatMap - Notes
class FlatMap_IntroViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
Error Throwing
@Published var nameResults: [NameResult] = []
I explicitly set the failure type of this pipeline to Never.
private var cancellables: Set<AnyCancellable> = []
I handle errors within flatMap. The replaceError
will convert the pipeline to a non-error-throwing
pipeline and set the failure type to Never.
func fetchNameResults() {
names.publisher
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
}
.flatMap { (name, url) -> AnyPublisher<NameResult, Never> in
I didn’t have to set the return type of flatMap. It will
work just fine without it but I wanted it here so you
could see it and it would be more clear.
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: NameResult.self, decoder: JSONDecoder())
You could throw an error from flatMap if you wanted
to. You would just have to change the subscriber from
sink(receiveValue:) to
sink(receiveCompletion:receiveValue:).
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
See more at “Handling Errors”.
.eraseToAnyPublisher()
}
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in
nameResults.append(nameResult)
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
The receive operator
switches execution back to the
main thread. If you don’t do
this, Xcode will show you a
purple warning and you may
or may not see results appear
on the UI.
369
Combine Mastery in SwiftUI
Working with Multiple Publishers
FlatMap - Order
class FlatMap_IntroViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
@Published var nameResults: [NameResult] = []
private var cancellables: Set<AnyCancellable> = []
func fetchNameResults() {
names.publisher
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
}
.flatMap { (name, url) -> AnyPublisher<NameResult, Never> in
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
Different
order
data
}
Use width: 214
.decode(type: NameResult.self, decoder: JSONDecoder())
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher()
}
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in
nameResults.append(nameResult)
}
.store(in: &cancellables)
}
}
www.bigmountainstudio.com
You can’t guarantee the order in which the results are
returned from this flatMap. All of the publishers can run
all at the same time.
You CAN control how many publishers can run at the same
time though with the maxPublishers parameter.
See next page…
370
Combine Mastery in SwiftUI
Working with Multiple Publishers
FlatMap - MaxPublishers
.flatMap(maxPublishers: Subscribers.Demand.max(1)) { (name, url) in
Setting maxPublishers tells flatMap how many of the publishers can run at the same time.
If set to 1, then one publisher will have to finish before the next one can begin.
Now the results are in the same order as the items that came down the pipeline.
Note: The default value for maxPublishers is:
Use width: 214
Subscribers.Demand.unlimited
www.bigmountainstudio.com
371
Combine Mastery in SwiftUI
Merge
Pipelines that send out the same type can be merged together so items that come from them will all come together and be sent down the same pipeline to the
subscriber. Using the merge operator you can connect up to eight publishers total.
Working with Multiple Publishers
Merge - View & View Model
struct Merge_Intro: View {
@StateObject private var vm = Merge_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Merge",
subtitle: "Introduction",
desc: "The merge operator can collect items of the same type from many
different publishers and send them all down the same pipeline.")
List(vm.data, id: \.self) { item in
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
Use width: 214
You can merge up to seven additional publishers of
the same type to your main publisher.
}
}
class Merge_IntroViewModel: ObservableObject {
@Published var data: [String] = []
func fetch() {
let artists = ["Picasso", "Michelangelo"]
let colors = ["red", "purple", "blue", "orange"]
let numbers = ["1", "2", "3"]
_ = artists.publisher
.merge(with: colors.publisher, numbers.publisher)
.sink { [unowned self] item in
data.append(item)
}
}
You can see from the
order on the
screenshot how these
sequence publishers
all got merged
together.
Other types of
merged publishers
will just publish their
items as they come
in.
}
www.bigmountainstudio.com
373
Combine Mastery in SwiftUI
SwitchToLatest
You use switchToLatest when you have a pipeline that has publishers being sent downstream. If you looked at the flatMap operator you will understand this
concept of a publisher of publishers. Instead of values going through your pipeline, it’s publishers. And those publishers are also publishing values on their own. With
the flatMap operator, you can collect ALL of the values these publishers are emitting and send them all downstream.
But maybe you don’t want ALL of the values that ALL of these publishers emit. Instead of having these publishers run at the same time, maybe you want just the
latest publisher that came through to run and cancel out all the other ones that are still running that came before it.
And that is what the switchToLatest operator is for. It’s kind of similar to combineLatest, where only the last value that came through is used. This is using the
last publisher that came through.
Working with Multiple Publishers
SwitchToLatest - View
struct SwitchToLatest_Intro: View {
@StateObject private var vm = SwitchToLatest_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("SwitchToLatest",
subtitle: "Introduction",
desc: "The switchToLatest operator will use only the latest publisher
that comes through the pipeline.")
Text(vm.names.joined(separator: ", "))
Button("Find Gender Probability") {
vm.fetchNameResults()
}
List(vm.nameResults, id: \.name) { nameResult in
HStack {
Text(nameResult.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(nameResult.gender + ": ")
Text(getPercent(nameResult.probability))
}
}
Use width: 214
}
.font(.title)
}
func getPercent(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
return formatter.string(from: NSNumber(value: number)) ?? "N/A"
}
}
www.bigmountainstudio.com
375
This example is very
similar to the flatMap
example except now it
uses map and
switchToLatest.
That’s why you only see
the last name, “Tracy”,
because it was the last
publisher that came
down the pipeline.
Combine Mastery in SwiftUI
Working with Multiple Publishers
SwitchToLatest - View Model
class SwitchToLatest_IntroViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
@Published var nameResults: [NameResult] = []
private var cancellables: Set<AnyCancellable> = []
Learn more about dataTaskPublisher here.
func fetchNameResults() {
Learn more about replaceError here.
names.publisher
Learn more about eraseToAnyPublisher in the
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
Organizing chapter.
}
.map { (name, url) in
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: NameResult.self, decoder: JSONDecoder())
Using the URL created with the name,
another publisher is created and sent
down the pipeline.
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in
nameResults.append(nameResult)
}
.store(in: &cancellables)
}
The receive operator switches execution
back to the main thread. If you don’t do this,
Xcode will show you a purple warning and you
may or may not see results appear on the UI.
The switchToLatest operator will only
republish the item published by the latest
dataTaskPublisher that came through.
OK, that’s a mouthful. Let’s look at a
diagram on the next page.
}
www.bigmountainstudio.com
376
Combine Mastery in SwiftUI
Working with Multiple Publishers
SwitchToLatest - Diagram
You are the latest
publisher. Publish your value and I
will send it down the pipeline.
Tracy
Taylor
Pipeline
1
All 6 publishers come in one after another
and only the latest one (the last one, in this
case) is used to publish its value.
Alexus
Pat
Publish
Tracy
Madison
Kelly
www.bigmountainstudio.com
2
struct NameResult: Decodable
{
var name = "Tracy"
var gender = "female"
var probability = 0.92
}
The dataTaskPublisher publishes its value
and sends it downstream.
377
Combine Mastery in SwiftUI
􀎷
Working with Multiple Publishers
SwitchToLatest: Cancels Current Publisher - View
struct SwitchToLatest_CancelsCurrentPublisher: View {
@StateObject private var vm = SwitchToLatest_CancelsCurrentPublisherViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("SwitchToLatest",
subtitle: "Cancels Current Publisher",
desc: "When the switchToLatest operator receives a new publisher, it will
cancel the current publisher it might have.")
List(vm.names, id: \.self) { name in
Button(name) {
vm.fetchNameDetail.send(name)
}
}
HStack {
Text(vm.nameResult?.name ?? "Select a name")
.frame(maxWidth: .infinity, alignment: .leading)
Text((vm.nameResult?.gender ?? "") + ": ")
Text(getPercent(vm.nameResult?.probability ?? 0))
}
.padding()
.border(Color("Gold"), width: 2)
}
.font(.title)
}
func getPercent(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
return formatter.string(from: NSNumber(value: number)) ?? "N/A"
}
}
www.bigmountainstudio.com
378
In this example, every
time you tap a row an
API is called to get
information.
If you tap many rows
then that could mean a
lot of network traffic.
Using
switchToLatest will
automatically cancel all
previous network calls
and only run the latest
one.
Combine Mastery in SwiftUI
Working with Multiple Publishers
SwitchToLatest: Cancels Current Publisher - View Model
class SwitchToLatest_CancelsCurrentPublisherViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
@Published var nameResult: NameResult?
var fetchNameDetail = PassthroughSubject<String, Never>()
A PassthroughSubject is the publisher this time.
Only one name will be sent through at a time. But many
names can come through.
private var cancellables: Set<AnyCancellable> = []
init() {
fetchNameDetail
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
To my surprise, this API was actually pretty fast so I
}
delayed it for half a second to give the
.map { (name, url) in
dataTaskPublisher a chance to get canceled by the
URLSession.shared.dataTaskPublisher(for: url)
switchToLatest operator.
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: NameResult.self, decoder: JSONDecoder())
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.delay(for: 0.5, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
.switchToLatest()
Learn More
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in
• dataTaskPublisher
self.nameResult = nameResult
• replaceError
}
If the user is tapping many rows, the switchToLatest
.store(in: &cancellables)
• delay
operator will keep canceling dataTaskPublishers until one
}
}
www.bigmountainstudio.com
finishes and then sends the results downstream.
379
• eraseToAnyPublisher
Combine Mastery in SwiftUI
Zip
Using the zip operator you can connect two pipelines and then use a closure to process the data from each publisher in some way. There is also a zip3 and zip4 to
connect even more pipelines together. You will still have just one pipeline after connecting all the pipelines that send down the data to your subscriber.
Working with Multiple Publishers
Zip - View
struct Zip_Intro: View {
@StateObject private var vm = Zip_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Zip",
subtitle: "Introduction",
desc: "You can combine multiple pipelines and pair up the values from
each one and do something with them using the zip operator.")
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 250))]) {
ForEach(vm.dataToView) { artData in
VStack {
Image(artData.artist)
.resizable()
.aspectRatio(contentMode: .fit)
Text(artData.artist)
.font(.body)
}
.padding(4)
.background(artData.color.opacity(0.4))
.frame(height: 150)
}
}
Use width: 214
}
.font(.title)
.onAppear {
vm.fetch()
}
There are two publishers, one for an artist’s name
and another for color.
The zip operator combines the values from these
two publishers and sends them down the pipeline.
They are used together to create the UI.
}
}
www.bigmountainstudio.com
381
Combine Mastery in SwiftUI
Working with Multiple Publishers
Zip - View Model
class Zip_IntroViewModel: ObservableObject {
@Published var dataToView: [ArtData] = []
func fetch() {
let artists = ["Picasso", "Michelangelo", "van Gogh", "da Vinci", "Monet"]
let colors = [Color.red, Color.orange, Color.blue, Color.purple, Color.green]
_ = artists.publisher
.zip(colors.publisher) { (artist, color) in
return ArtData(artist: artist, color: color)
}
.sink { [unowned self] (item) in
Use width: 214
dataToView.append(item)
}
Note: Items only get
published when there is a
value from BOTH publishers.
}
}
If you were to remove
Color.green from the
colors array then “Monet”
would not get published. It is
because “Monet" would not
have a matching value from
the colors array anymore.
The zip operator will match up items from each publisher and pass
them as input parameters into its closure.
In this example, both input parameters are used to create a new
ArtData object and then send that down the pipeline.
www.bigmountainstudio.com
382
?
Combine Mastery in SwiftUI
HANDLING ERRORS
Handling Errors
About Error Handling
Do I need error handling on all of my pipelines?
No, you do not. There are two types of pipelines:
🧨 Error-Throwing Pipelines
🟢 Non-Error-Throwing Pipelines
There are publishers and operators that can throw errors. Operators that
begin with “try” are good examples. Xcode will let you add error handling
to these pipelines.
There are pipelines that never throw errors. They have publishers that are
incapable of throwing errors and downstream there are no “try”
operators that throw errors. Xcode will NOT let you add error handling to
these pipelines.
publisher
.try… { … }
.sink(receiveCompletion: { … },
receiveValue: { … })
publisher
.map { … }
.sink(receiveValue: { … })
// OR
.assign(to: )
Xcode will not allow you to use just sink(receiveValue:) if it is an
error-throwing pipeline. You need receiveCompletion (like you see
in the example above) to handle the error that caused the failure.
You also cannot use assign(to:). That subscriber is for non-error
throwing pipelines only. Xcode will show you an error if you try.
www.bigmountainstudio.com
Xcode WILL allow you to use sink(receiveValue:), or
sink(receiveCompletion:receiveValue:), or assign(to:).
The assign(to:) subscriber is for non-error throwing pipelines only.
384
Combine Mastery in SwiftUI
Handling Errors
Can I change error-throwing pipelines into non-error-throwing?
Yes! This can go both ways. You can change error-throwing pipelines into pipelines that never throw errors. And you can turn pipelines that never throw
errors into error-throwing pipelines just by adding one of the many “try” operators.
In this chapter, you will see many error handling operators that can turn an error-throwing pipeline into a pipeline that never throws an error.
Non-error-throwing publisher
This error handling operator changes this error-throwing pipeline back into a pipeline
that never throws an error. Many operators in this chapter show you how to do this.
!
error
try
This subscriber now expects no errors and so
can use either sink(receiveValue:) or
assign(to:).
A try operator that turns this
into an error-throwing pipeline.
www.bigmountainstudio.com
385
Combine Mastery in SwiftUI
Handling Errors
How can I tell if a pipeline is error-throwing or not?
Publishers and operators can both throw errors. How do you know which ones throw errors? Well, here are some tips!
Tips for detecting Error-throwing Subscribers/Operators
All operators that begin with
“try“ throw errors.
So far, the only publisher I know that can
throw an error is the dataTaskPublisher.
Try adding an assign(to:) subscriber. If Xcode gives
you an error, then usually something is throwing an error.
OPTION+click a publisher/operator and view the help documentation and look
for words like “throw” or “error”.
(Decode operator)
www.bigmountainstudio.com
386
Combine Mastery in SwiftUI
AssertNoFailure
error
==
You use the assertNoFailure operator to ensure there will be no errors caused by anything upstream from it. If there is, your app will then crash. This is best to use
when developing when you need to make sure that your data is always correct and your pipeline will always work.
Once your app is ready to ship though, you may want to consider removing it or it can crash your app if there is a failure.
Handling Errors
AssertNoFailure - View
struct AssertNoFailure_Intro: View {
@StateObject private var vm = AssertNoFailure_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("AssertNoFailure",
subtitle: "Introduction",
desc: "The assertNoFailure operator will crash your app if there is a
failure. This will make it very obvious while developing so you
can easily find and fix the problem.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
Consider this Scenario
While developing you might see a 🧨 in your data. You got it
fixed and are certain it should never reappear.
So you can add an assertNoFailure to your pipeline while
continuing your development.
www.bigmountainstudio.com
388
Combine Mastery in SwiftUI
Handling Errors
AssertNoFailure - View Model
class AssertNoFailure_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
If you run this code as it is, Xcode will halt execution and display this error:
func fetch() {
let dataIn = ["Value 1", "Value 2", "🧨 ", "Value 3"]
_ = dataIn.publisher
.tryMap { item in
// There should never be a 🧨 in the data
if item == "🧨 " {
throw InvalidValueError()
}
Throwing this error will make your app crash because
you are using the assertNoFailure operator.
return item
}
.assertNoFailure("This should never happen.")
.sink { [unowned self] (item) in
dataToView.append(item)
}
You have seen from the many examples where a try operator is used that Xcode
forces you to use the sink(receiveCompletion:receiveValue:) subscriber
because you have to handle the possible failure.
}
But in this case, the assertNoFailure tells the downstream pipeline that no
failure will be sent downstream and therefore we can just use
sink(receiveValue:).
}
www.bigmountainstudio.com
389
Combine Mastery in SwiftUI
Catch
!
error
try
The catch operator has a very specific behavior. It will intercept errors thrown by upstream publishers/operators but you must then specify a new publisher that will
publish a new value to go downstream. The new publisher can be to send one value, many values, or do a network call to get values. It’s up to you.
The one thing to remember is that the publisher you specify within the catch’s closure must return the same type as the upstream publisher.
Handling Errors
Catch - View
struct Catch_Intro: View {
@StateObject private var vm = Catch_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Catch",
subtitle: "Introduction",
desc: "Use the catch operator to intercept errors thrown upstream and
specify a publisher to publish new data from within the provided
closure.")
.layoutPriority(1)
Use width: 214
List(vm.dataToView, id: \.self) { item in
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
When fetching data the pipeline
encounters invalid data and throws an
error. The catch intercepts this and
publishes “Error Found”.
391
Combine Mastery in SwiftUI
Handling Errors
Catch - View Model
struct BombDetectedError: Error, Identifiable {
let id = UUID()
}
Error to throw.
class Catch_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"]
_ = dataIn.publisher
.tryMap{ item in
if item == "🧨 " {
Use width: 214
throw BombDetectedError()
}
return item
}
Using the Just publisher to send
another value downstream.
.catch { (error) in
Just("Error Found")
}
.sink { [unowned self] (item) in
dataToView.append(item)
}
}
}
www.bigmountainstudio.com
Important Note
Catch will intercept and replace the upstream publisher.
“Replace” is the important word here.
?
This means that the original publisher will not publish any
other values after the error was thrown because it was
replaced with a new one.
392
Combine Mastery in SwiftUI
TryCatch
!
!
error
error
try
If you want the ability of the catch operator but also want to be able to throw an error, then tryCatch is what you need.
Handling Errors
TryCatch - View
struct TryCatch_Intro: View {
@StateObject private var vm = TryCatch_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("TryCatch",
subtitle: "Introduction",
desc: "The tryCatch operator will work just like catch but also allow you
to throw an error within the closure.")
.layoutPriority(1)
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text("Failed fetching alternate data."))
}
.onAppear {
vm.fetch()
}
We’re going to fetch data and run into some bad
data. The catch operator will fetch alternate data
which will also fail, resulting in showing this alert.
}
}
www.bigmountainstudio.com
394
Combine Mastery in SwiftUI
Handling Errors
TryCatch - View Model
class TryCatch_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
@Published var error: BombDetectedError?
struct BombDetectedError: Error, Identifiable {
let id = UUID()
}
func fetch() {
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"]
_ = dataIn.publisher
.tryMap{ item in
if item == "🧨 " {
Can I use tryMap on a non-error throwing pipeline?
throw BombDetectedError()
No. Upstream from the tryCatch has to be some operator or publisher
}
that is capable of throwing errors. That is why you see tryMap upstream
return item
from tryCatch. Otherwise, Xcode will give you an error.
}
.tryCatch { [unowned self] (error) in
fetchAlternateData()
}
.sink { [unowned self] completion in
if case .failure(let error) = completion {
When fetch tries to get data it runs into a problem, throws an
self.error = error as? BombDetectedError
}
error, and then tryCatch calls another publisher that also
} receiveValue: { [unowned self] item in
throws an error.
dataToView.append(item)
}
}
In the end, the sink subscriber is handling the error from
func fetchAlternateData() -> AnyPublisher<String, Error> {
["Alternate Value 1", "Alternate Value 2", "🧨 ", "Alternate Value 3"]
.publisher
.tryMap{ item -> String in
if item == "🧨 " { throw BombDetectedError() }
return item
}
.eraseToAnyPublisher()
}
fetchAlternateData.
}
www.bigmountainstudio.com
395
Combine Mastery in SwiftUI
MapError
!
error
"
$
error
#
try
try
error
try
You can have several parts of your pipeline throw errors. The mapError operator allows a central place to catch them before going to the subscriber and gives you a
closure to throw a new error. For example, you might want to be able to receive 10 different types of errors and then throw one generic error instead.
Handling Errors
MapError - View
struct MapError_Intro: View {
@StateObject private var vm = MapError_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("MapError",
subtitle: "Introduction",
desc: "The mapError operator provides a closure to receive an upstream
error and then republish another error.")
Button("Fetch Data") {
vm.fetch()
}
Use width: 214
List(vm.todos) { todo in
Label(title: { Text(todo.title) },
icon: { Image(systemName: todo.completed ?
"checkmark.circle.fill" :
"circle") })
}
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}
www.bigmountainstudio.com
397
If the pipeline throws any
errors then the
mapError will receive
and republish another
error.
It will be assigned to this
error published
property.
When the alert modifier
detects a value, it will
present an alert to the
user.
Combine Mastery in SwiftUI
Handling Errors
MapError - View Model
Note: This view model is a little bit longer and continues on to the next page.
class MapError_IntroViewModel: ObservableObject {
@Published var todos: [ToDo] = []
@Published var error: ErrorForView?
private var cancellable: AnyCancellable?
struct ErrorForView: Error, Identifiable {
let id = UUID()
var message = ""
}
struct ToDo: Identifiable, Decodable {
var id: Int
var title: String
var completed: Bool
}
func fetch() {
let url = URL(string: "https://jsonplaceholder.typicode.com/users/1/todos")!
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap { (data: Data, response: URLResponse) -> Data in
Using the dataTaskPublisher for this example
because it can throw an error and we can
throw more errors depending on the
response.
guard let httpResponse = response as? HTTPURLResponse else {
throw UrlResponseErrors.unknown
Check the response codes to see if there were any problems and
throw an error. Here is the error object:
}
if (400...499).contains(httpResponse.statusCode) {
throw UrlResponseErrors.clientError
enum UrlResponseErrors: String, Error {
case unknown = "Response wasn't recognized"
case clientError = "Problem getting the information"
case serverError = "Problem with the server"
case decodeError = "Problem reading the returned data"
}
}
if (500...599).contains(httpResponse.statusCode) {
throw UrlResponseErrors.serverError
}
return data
}
Note: The decode operator can also throw an error.
.decode(type: [ToDo].self, decoder: JSONDecoder())
www.bigmountainstudio.com
398
Combine Mastery in SwiftUI
Handling Errors
You can see that mapError receives an error and the
closure is set to ALWAYS return a UrlResponseErrors
type. (See the previous page for this object.)
.mapError { error -> UrlResponseErrors in
if let responseError = error as? UrlResponseErrors {
return responseError
} else {
So mapError can receive many different types of errors
and you control the type that gets sent downstream.
return UrlResponseErrors.decodeError
}
}
If there is an error that enters the sink subscriber, you
already know it will be of type UrlResponseErrors
because that is what the mapError is returning:
.receive(on: RunLoop.main)
.sink { [unowned self] completion in
if case .failure(let error) = completion {
self.error = ErrorForView(message: error.rawValue)
}
} receiveValue: { [unowned self] data in
todos = data
}
}
}
Note: In the mapError example I’m assuming if the error received is NOT a
UrlResponseErrors type then an error came from the decode operator.
But remember, the dataTaskPublisher could also throw an error.
So if you do use mapError, be sure to check the type of the error received
so you know where it’s coming from before changing it in some way.
www.bigmountainstudio.com
399
The receive operator switches execution back to the
main thread. If you don’t do this, Xcode will show you a
purple warning and you may or may not see results
appear on the UI.
Combine Mastery in SwiftUI
ReplaceError
!
error
try
Instead of showing an alert on the UI, you could use the replaceError operator to substitute a value instead. If you have a pipeline that sends integers down the
pipeline and there’s an operator that throws an error, then you can use replaceError to replace the error with a zero, for example.
Handling Errors
ReplaceError - View
struct ReplaceError_Intro: View {
@StateObject private var vm = ReplaceError_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("ReplaceError",
subtitle: "Introduction",
desc: "The replaceError operator will replace any error received with
another value you specify.")
List(vm.dataToView, id: \.self) { item in
Text(item)
Use width: 214
.foregroundColor(item == vm.replacedValue ? .red : .primary)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
The idea here is that if an error is encountered in the pipeline
then it will be replaced with “Error Found”.
}
When an error is encountered, the pipeline finishes, and no
more data passes through it.
www.bigmountainstudio.com
401
Combine Mastery in SwiftUI
Handling Errors
ReplaceError - View Model
class ReplaceError_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
var replacedValue = "Error Found"
func fetch() {
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"]
You will not see these values published because
the pipeline will finish after replaceError is called.
_ = dataIn.publisher
.tryMap{ item in
if item == "🧨 " {
throw BombDetectedError()
}
struct BombDetectedError: Error, Identifiable
{
let id = UUID()
}
return item
}
.replaceError(with: replacedValue)
.sink { [unowned self] (item) in
dataToView.append(item)
Notice you do not have to use sink(receiveCompletion:receiveValue:). This is
because replaceError turned the pipeline into a non-error-throwing pipeline.
}
}
}
www.bigmountainstudio.com
402
Combine Mastery in SwiftUI
Retry
failure?
2
As your pipeline is trying to publish items an error could be encountered. Normally the subscriber receives that error. With the retry operator though, the failure
will not reach the subscriber. Instead, it will have the publisher try to publish again a certain number of times that you specify.
Handling Errors
Retry - View
struct Retry_Intro: View {
@StateObject private var vm = Retry_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Retry",
subtitle: "Introduction",
desc: "The retry operator will detect failures and attempt to run the
publisher again the number of times you specify.")
Text(vm.webPage)
Use width: 214
.padding()
The webPage property will either show the HTML it retrieved
from a website or an error message.
Spacer(minLength: 0)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
www.bigmountainstudio.com
404
Combine Mastery in SwiftUI
Handling Errors
Retry - View Model
class Retry_IntroViewModel: ObservableObject {
@Published var webPage = ""
private var cancellable: AnyCancellable?
func fetch() {
let url = URL(string: "https://oidutsniatnuomgib.com/")!
Just because the retry is set to 2, the publisher will
actually get run 3 times.
The publisher runs the first time, fails, then runs 2 more
times to retry.
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.retry(2)
.map { (data: Data, response: URLResponse) -> String in
String(decoding: data, as: UTF8.self)
}
The receive operator switches execution back to the
main thread. If you don’t do this, Xcode will show you a
purple warning and you may or may not see results
appear on the UI.
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
if case .failure(_) = completion {
webPage = "We made 3 attempts to retrieve the webpage and failed."
}
}, receiveValue: { [unowned self] html in
webPage = html
})
}
}
www.bigmountainstudio.com
405
Combine Mastery in SwiftUI
DEBUGGING
Breakpoint
1
2
3
4
5
You can set conditions in your pipelines to have the app break during execution using the breakpoint operator. Note: This is not the same as setting a
breakpoint in Xcode. Instead, what happens is Xcode will suspend the process of execution because this breakpoint operator is actually raising what’s called a
SIGTRAP (signal trap) to halt the process. A “signal” is something that happens on the CPU level. Xcode is telling the processor, “Hey, let me know if you run this code
and this condition is true and halt the process.” When the processor finds your code and the condition is true, it will “trap” the process and suspend it so you can take
a look in Xcode.
Debugging
Breakpoint - View
struct Breakpoint_Intro: View {
@StateObject private var vm = Breakpoint_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Breakpoint",
subtitle: "Introduction",
desc: "The breakpoint operator allows you to set conditions on different
events so Xcode will pause when those conditions are satisfied.")
List(vm.dataToView, id: \.self) { item in
Text(item)
Use width: 214
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
In this example, we want Xcode to pause
execution when it encounters a % in the values.
www.bigmountainstudio.com
408
Combine Mastery in SwiftUI
Debugging
Breakpoint - View Model
class Breakpoint_IntroViewModel: ObservableObject {
You don’t need to include all three parameters
(closures). Just use the ones you want to examine.
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Mercury", "Venus", "%Haley's Comet%", "Earth"]
Return true if you want Xcode to pause execution.
_ = dataIn.publisher
.breakpoint(
receiveSubscription: { subscription in
print("Subscriber has connected")
return false
},
receiveOutput: { value in
print("Value (\(value)) came through pipeline")
return value.contains("%")
},
receiveCompletion: { completion in
print("Pipeline is about to complete")
You can see the order of the events here:
return false
}
)
.sink(receiveCompletion: { completion in
print("Pipeline completed")
}, receiveValue: { [unowned self] item in
dataToView.append(item)
})
}
}
www.bigmountainstudio.com
Xcode Debugger Console
409
Combine Mastery in SwiftUI
Debugging
Breakpoint - Xcode
Here’s what you’re looking at when you return true from the breakpoint operator. Xcode suspends execution and you see this:
Where it happened
While the SIGTRAP information might not be so helpful, the stack trace might be.
You can click on the next item with the purple icon (13) to see which file threw the
SIGTRAP and go from there.
www.bigmountainstudio.com
410
At this point, I would find where it was thrown
and then add Xcode breakpoints to more closely
examine the code.
Combine Mastery in SwiftUI
BreakpointOnError
!
error
1
2
3
4
5
try
Use the breakpointOnError when you are interested in having Xcode pause execution when ANY error is thrown within your pipeline. While developing, you may
have a pipeline that you suspect should never throw an error so you don’t add any error handling on it. Instead, you can add this operator to warn you if your
pipeline did throw an error when you were not expecting it to.
Debugging
BreakpointOnError - View
struct BreakpointOnError_Intro: View {
@StateObject private var vm = BreakpointOnError_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("BreakpointOnError",
subtitle: "Introduction",
desc: "Use the breakpointOnError operator to have Xcode pause execution
whenever an error is thrown from the pipeline.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
In this example, an error is thrown if the pipeline
gets what it considers invalid data.
.font(.title)
.onAppear {
During development, if you get invalid data, you
want to tell your data team that it needs to be
corrected before releasing the app.
vm.fetch()
}
}
So you can use breakpointOnError to your
pipeline to warn you of invalid data (or whatever
else you don’t expect).
}
www.bigmountainstudio.com
412
Combine Mastery in SwiftUI
Debugging
BreakpointOnError - View Model
class BreakpointOnError_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
func fetch() {
let dataIn = ["Mercury", "Venus", "Earth", "Pluto"]
Your assumption is that this
should never happen. If it does
happen, Xcode will pause
execution with the debugger.
If an error is thrown, Xcode will pause
execution and show you this window. I
recommend looking at the stack trace to find
where it originated from.
_ = dataIn.publisher
.tryMap { item in
if item == "Pluto" {
throw InvalidPlanetError()
}
return item
}
.breakpointOnError()
.sink(receiveCompletion: { completion in
print("Pipeline completed")
}, receiveValue: { [unowned self] item in
dataToView.append(item)
})
Error thrown will
be in here
}
}
www.bigmountainstudio.com
413
Combine Mastery in SwiftUI
HandleEvents
There are some events you have access to with the sink subscriber such as when it receives a value or when it cancels or completes. But what if you’re not using a
sink subscriber or if you need access to other events such as when a subscription is received or a request is received?
This is where the handleEvents operator can become useful. It is one operator that can expose 5 different events and give you closures for each one so you can write
debugging code or other code as you will see in the following examples.
Debugging
HandleEvents - View
struct HandleEvents: View {
@StateObject private var vm = HandleEventsViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("HandleEvents",
subtitle: "Introduction",
desc: "Use the handleEvents operator to get a closer look into what is
happening at each stage of your pipeline.")
List(vm.dataToView, id: \.self) { item in
Use width: 214
Text(item)
}
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
The pipeline for this List is getting planets and is
throwing an error when it detects Pluto.
www.bigmountainstudio.com
415
Combine Mastery in SwiftUI
Debugging
HandleEvents - View Model
class HandleEventsViewModel: ObservableObject {
@Published var dataToView: [String] = []
You are given a closure for each of the five
events. They are all optional so just use the
ones you want.
func fetch() {
let dataIn = ["Mercury", "Venus", "Earth", "Pluto"]
_ = dataIn.publisher
.handleEvents(
receiveSubscription: { subscription in
print("Event: Received subscription")
}, receiveOutput: { item in
print("Event: Received output: \(item)")
}, receiveCompletion: { completion in
print("Event: Pipeline completed")
}, receiveCancel: {
print("Event: Pipeline cancelled")
}, receiveRequest: { demand in
print("Event: Received request")
})
.tryMap { item in
if item == "Pluto" {
throw InvalidPlanetError()
}
return item
}
.sink(receiveCompletion: { completion in
print("Pipeline completed")
}, receiveValue: { [unowned self] item in
dataToView.append(item)
})
Note: The receiveCompletion in this
example will not execute because there is an
error is being thrown (Pluto).
You can see the output for the events here:
}
}
Xcode Debugger Console
www.bigmountainstudio.com
416
Combine Mastery in SwiftUI
Debugging
HandleEvents for Showing Progress - View
struct HandleEvents_Progress: View {
@StateObject private var vm = HandleEvents_ProgressViewModel()
var body: some View {
ZStack {
VStack(spacing: 20) {
HeaderView("HandleEvents",
subtitle: "Showing Progress",
desc: "You can also use handleEvents to hide and show views. In this
example a ProgressView is shown while fetching data.")
Form {
Section(header: Text("Bitcoin Price").font(.title2)) {
HStack {
Text("USD")
.frame(maxWidth: .infinity, alignment: .leading)
Text(vm.usdBitcoinRate)
.layoutPriority(1)
}
}
}
Use width: 214
}
if vm.isFetching {
ProcessingView()
}
}
.font(.title)
.onAppear {
vm.fetch()
}
The handleEvents operator sets the
isFetching property.
Note: You can see the code for
ProcessingView here.
}
}
www.bigmountainstudio.com
417
Combine Mastery in SwiftUI
Debugging
HandleEvents for Showing Progress - View Model
class HandleEvents_ProgressViewModel: ObservableObject {
@Published var usdBitcoinRate = ""
@Published var isFetching = false
This is the struct the JSON is decoding into:
struct BitcoinPrice: Decodable {
let bpi: Bpi
func fetch() {
let url = URL(string: "https://api.coindesk.com/v1/bpi/currentprice.json")!
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
}
.decode(type: BitcoinPrice.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.handleEvents(receiveCompletion: { [unowned self] _ in
isFetching = false
}, receiveCancel: { [unowned self] in
isFetching = false
}, receiveRequest: { [unowned self] _ in
isFetching = true
})
.map{ bitcoinPrice in
bitcoinPrice.bpi.USD.rate // Return just the USD rate
}
.catch { _ in
Just("N/A")
}
.assign(to: &$usdBitcoinRate)
}
}
www.bigmountainstudio.com
418
struct Bpi: Decodable {
let USD: Rate
let GBP: Rate
let EUR: Rate
struct Rate: Decodable {
let rate: String
}
}
}
The pipeline could complete normally or be canceled
so isFetching is set to false in both cases.
Learn more about these
publishers and operators:
• dataTaskPublisher
• catch
Combine Mastery in SwiftUI
Print
The print operator is one of the quickest and easiest ways to get information on what your pipeline is doing. Any publishing event that occurs will be logged by the
print operator on your pipeline.
Debugging
Print
class Print_IntroViewModel: ObservableObject {
@Published var data: [String] = []
private var cancellable: AnyCancellable?
init() {
let dataIn = ["Bill", nil, nil, "Emma", nil, "Jayden"]
cancellable = dataIn.publisher
.print()
.replaceNil(with: "<Needs ID>")
.sink { [unowned self] datum in
self.data.append(datum)
}
Simply add print() to start printing all
events related to this pipeline to the
debug console.
}
}
Use width: 214
struct Print_Intro: View {
@StateObject private var vm = UsingPrint_IntroViewModel()
var body: some View {
VStack(spacing: 20) {
HeaderView("Using Print",
subtitle: "Introduction",
desc: "The print operator can
reveal everything that is happening with your pipeline,
including how it is connected and what is going through
it.")
List(vm.data, id: \.self) { datum in
Text(datum)
}
}
.font(.title)
}
}
www.bigmountainstudio.com
420
Combine Mastery in SwiftUI
Testing for Memory Leaks
In this section, you will see a way to test your views unloading from memory and verifying if the observable object is also unloading with it (which it should). The main
goal is to make sure your Combine pipelines aren’t causing your objects to be retained in memory.
Debugging
Testing for Memory Leaks - View
struct TestingMemory_UsingSheet: View {
@State private var showSheet = false
var body: some View {
VStack(spacing: 20) {
HeaderView("Testing Memory",
subtitle: "Using Sheet",
desc: "When a view de-initializes, its view model should also deinitialize. One way to easily test this is by using a sheet to
present the view you are testing.")
Button("Show Sheet") {
Use width: 214
showSheet.toggle()
}
DescView("When you dismiss the sheet (which contains the view you are testing), its
view model should be de-initialized.")
}
.font(.title)
.sheet(isPresented: $showSheet) {
TestingMemoryView()
}
}
See on the next page how to test if the view
model is de-initialized.
}
www.bigmountainstudio.com
422
Combine Mastery in SwiftUI
Debugging
Testing for Memory Leaks - View Model
class TestingMemory_ViewModel: ObservableObject {
Add a deinit function to your view model.
This function is called right before the class
is removed from memory.
If it does not get run, you know you
have a memory leak.
@Published var data = ""
func fetch() {
data = "New value"
}
deinit {
print("Unloaded TestingMemory_ViewModel")
}
}
struct TestingMemoryView: View {
@StateObject private var vm = TestingMemory_ViewModel()
Use width: 214
var body: some View {
VStack {
DescView("This would be the view you are testing. Drag down to dismiss and you
should see the view model get de-initialized.")
Text(vm.data)
Look in your Xcode debugger console for the
print message.
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}
(Xcode Debugger Console)
www.bigmountainstudio.com
423
Combine Mastery in SwiftUI
MORE RESOURCES
Big Mountain Studio creates premium reference materials. This means my books are more like dictionaries that show individual topics. I highly recommend you
supplement your learning with tutorial-based learning too. Included in the following pages are more Combine learning resources that this book complements. Enjoy!
More Resources
Learning From Others - Books
Practical Combine
Using Combine
An introduction to Combine with real
examples
By Joseph Heck
This book explains the core concepts, provides
examples and sample code, and provides a reference to
the variety of tools that Combine makes available under
its umbrella.
By Donny Wals
Learn Combine from the ground up with a solid
theoretical foundation and real-world examples of
where and how Combine can help you move from
writing imperative code to writing reactive code that is
flexible, clean and modern.
Combine
A Combine Kickstart
Asynchronous Programming with Swift
By Daniel Steinberg
By Florent Pillet, Shai Mishali, Scott Gardner, Marin
Todorov
This hand-on, fast-moving kickstart introduces you to
the future of declarative and reactive programming on
Apple platforms. We focus on core concepts and
building discrete, easy-to-understand, pieces of a
pipeline that allows your app to react to changes in the
state.
Writing asynchronous code can be challenging, with a
variety of possible interfaces to represent, perform, and
consume asynchronous work — delegates, notification
center, KVO, closures, etc. Juggling all of these different
mechanisms can be somewhat overwhelming. Does it
really have to be this hard? Not anymore!
Note: Some of these are affiliate links.
www.bigmountainstudio.com
425
Combine Mastery in SwiftUI
More Resources
Learning From Others - Video Course
Combine Framework Course
A Swifty Combine Framework Course
By Karin Prater
Master Combine with great coding examples in UIKit
and SwiftUI. Discover all the tools you need to write
beautiful, readable, and workable code.
designcode.io
The Combine tutorials are minimal but the design
aspect with SwiftUI is phenomenal.
I listed this resource if you need help in designing
your UI and learning SwiftUI at the same time.
www.bigmountainstudio.com
426
Combine Mastery in SwiftUI
THE END
Good job!
MORE FROM ME
I have some other things you might also be interested in!
Go to Big Mountain Studio to discover more.
More From Me
Explorers Club
GET EXCLUSIVE ACCESS TO OVER $1,000 WORTH OF BOOKS AND COURSES
Get over $1,000 worth of products
Videos Library to quickly answer questions
Start your Combine journey
Learn how to build ANY mobile design in
SwiftUI
Build your online portfolio
Live demonstrations of SwiftUI
Over 250 videos
JOIN THE CLUB!
429
PARTNER PROGRAM
An “partner” is someone officially connected to me and Big Mountain Studio. As a partner you can sell my products with your own partner link. If someone buys a
product, you get:
20% !
If five people buy this book then you made your money back! Beyond that, you have got yourself some extra spending money. 💰
I love it, sign me up!
Just go to
and sign up. You will need a PayPal account to get paid.
Download
Study collections