0% found this document useful (1 vote)
4K views437 pages

Combine Mastery in SwiftUI

Uploaded by

joysarkar0898
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (1 vote)
4K views437 pages

Combine Mastery in SwiftUI

Uploaded by

joysarkar0898
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 437

VISUAL TIME-SAVING REFERENCE

Combine Mastery
iOS 18

In SwiftUI
Mark Moeykens
www.bigmountainstudio.com A COMBINE REFERENCE GUIDE
1 FOR SWIFTUI DEVELOPERS Big Mountain
Combine MasteryStudio
in SwiftUI
Version: 9-SEPTEMBER-2024
©2023 Big Mountain Studio LLC - All Rights Reserved

www.bigmountainstudio.com 2 Combine Mastery in SwiftUI


Special Thanks from Big Mountain Studio
As a member of Big Mountain Studio you now get a 20% off loyalty discount on all of these books!

$34 $27.20 $55 $44.00 $97 $77.60

$55 $44.00 $97 $77.60 $233 $186.40

CLICK HERE TO GET 20% OFF

3
ACKNOWLEDGMENTS
Writing a book while also working a full-time job takes time away from Many other developers also proof-read and gave feedback on the
family and friends. So I would like to thank them and the following. book. These include: Tim Barrett, Florian Schweizer, Chaithra
Pabbathi, Ron Avitzur, Mariusz, Udin Rajkarnikar, Jeff Deimund, Steve
Thanks to my patrons for their feedback, especially: Stewart Lynch, Zhou, Shane Miller, Thomas Swatland, Nadheer Chatharoo, Marco
Chris Parker, Basil, Herman Vermeulen, Franklin Byaruhanga, Paul Mayen (Kross), Pushpinder Pal Singh, Mats Braa, Eric, Schofield,
Colton for coming up with the first pipeline example, Jim Fetters, Stanislav Kasprik, Sev Moreno Breser, Mahmoud Ashraf, Sebastian
Mariusz Bohdanowicz, Ronnie Pitman, Marlon Simons, Emin Grbo, and Vidrea, Peter Pohlmann, Erica Gutierrez, Stephen Zyszkiewicz, Alireza
Rob In der Maur. Toghyiani, David Hosier, and Luke Smith.

I would also like to thank my friends who always gave me constant And finally, I would like to thank the creators of all the other sources
feedback, support, and business guidance: Chris Ching, Scott Smith, of information, whether Swift or Combine, that really helped me out
Rod Liberal, Chase Blumenthal, Lem Guerrero, and Chris Durtschi. and enabled me to write this book. That includes Apple and their
documentation and definition files, Shai Mishali, Marin Todorov, Donny
I would also like to thank the Utah developer community for their Wals, Karin Prater, Antoine van der Lee, Paul Hudson, Joseph Heck,
help in making this book possible. This includes Dave DeLong, Vadim Bulavin, Daniel Steinberg and Meng To.
Parker Wightman, Dave Nutter, Lem Guerrero, Chris Evans, and
BJ Homer.
TABLE OF CONTENTS
The table of contents should be built into your ePub and PDF readers. Examples:

Books App 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
1 I am using a custom view to format the title (1), subtitle (2), and descriptions (3) for the
examples in this book.
2
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.)
3

See next
struct Using_iOS: View { page for this
var body: some View { code.
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 8 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 .padding()
var body: some View { .background(Color.gold)
VStack(spacing: 15) { .foregroundStyle(.white)
if !title.isEmpty { }
Text(title) }
.font(.largeTitle) 1
}

Text(subtitle)
2 .foregroundStyle(.gray)

DescView(desc)
}
}
}

www.bigmountainstudio.com 9 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 10 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 11 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.




Architecture

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

Model View Model View


struct BookModel: Identifiable { class BookViewModel: ObservableObject { struct BookListView: View {
var id = UUID() @Published var books = [BookModel]() @StateObject var vm = BookViewModel()
var name = ""
} func fetch() { var body: some View {
books = List(vm.books) { book in
HStack {
[BookModel(name: "SwiftUI Views"),
Image(systemName: "book")
BookModel(name: "SwiftUI Animations"),
Text(book.name)
BookModel(name: "Data in SwiftUI"), }
BookModel(name: "Combine Reference")] }
} .onAppear {
} vm.fetch()
}
}
}

[ , , , ]

www.bigmountainstudio.com 13 Combine Mastery in SwiftUI


􀉚
􀉚
􀉚
􀉚
􀉚
Architecture

Model

struct BookModel: Identifiable { I use the Model to hold all the data needed to represent one thing.
var id = UUID()
var name = "" 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 14 Combine Mastery in SwiftUI


􀉚





Architecture

View Model

class BookViewModel: ObservableObject { The View Model is responsible for collecting your data and getting it ready to be presented on
@Published var books = [BookModel]() the view. It will notify the view of data changes so the view knows to update itself.

func fetch() { This is where you may or may not see things such as:
books = • Notifications to the view when data changes
[BookModel(name: "SwiftUI Views"),
• Updates to the data it exposes to the view (@Published property, in this example)
BookModel(name: "SwiftUI Animations"),
• Logic to validate data (may or may not be in the model)
BookModel(name: "Data in SwiftUI"),
BookModel(name: "Combine Reference")]
• 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 If separating out validation logic makes your life easier because it then becomes easier to test
@Published then you might want to read “Working with or reuse in other places, then do it.
Data in SwiftUI”.
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
@Published will also be covered later in this book. around or flip pages to connect all the dots.

www.bigmountainstudio.com 15 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
A Different Way of Thinking
HStack {
In SwiftUI, if you want to change what is showing on the
Image(systemName: "book")
screen then you ll have to change some data that drives
Text(book.name)
the UI.
}
}
.onAppear { Many of you, including myself, had to change the way we
vm.fetch() thought about the View.
}
} You can t reference UI elements and then access their
Use width: 214 } 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 In this example, I can t say List.add(newBook) to add
used in this book, I mostly use a view and an a new row on the list.
observable object. Sometimes you will see
data objects.
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 16 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 Pipeline Water User


(Water Tower)

www.bigmountainstudio.com 18 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 Something that can receive data from a publisher.
all at once or over time. In English, “subscribe” means to “arrange to receive
In English, “publish” means to “produce and send out something”.
to make known”.

I would like to sign up


for some data.

Sends data through the Pipeline

Publisher Subscriber

www.bigmountainstudio.com 19 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 Filter Pressure Subscriber


Operator Operator

www.bigmountainstudio.com 20 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 “Downstream” means “in the direction of the NEXT
part”. part”.
In Combine, the previous part is usually a Publisher or In Combine, the next part could be another Publisher,
Operator. Operator or even the Subscriber at the end.

I have 2 operators and a


subscriber downstream from me.

Upstream Downstream

Publisher Filter Pressure Subscriber


Operator Operator

www.bigmountainstudio.com 21 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 With Combine, you start with a Publisher and you can add many operators
View. (modifiers) to that Publisher.

Each modifier returns a NEW, modified View: Each operator returns a NEW, modified operator:

Text("Hello, World!") MyStringArrayPublisher


.font(.largeTitle) .fakeOperatorToRemoveDuplicates()
.bold() .fakeOperatorToRemoveNils()
.underline() .fakeOperatorToFilterOutItems(thatBeginWith: “m”)
.foregroundStyle(.green) .fakeOperatorToPublishTheseItemsEvery(seconds: 2)
.padding() .fakeSubscriberToAssignThisVariable(myResultVariable)

(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 22 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 {
By itself, a protocol does nothing and does not
var firstName: String { get set }
var lastName: String { get set } contain any logic.

func getFullName() -> String It simply defines properties and functions.


}

struct DeveloperStruct: PersonProtocol {


var firstName: String
var lastName: String This struct “conforms to” or implements the
protocol. Meaning, it is required that all the
func getFullName() -> String { properties and functions are used within it.
return firstName + " " + lastName
}
}

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 26 Combine Mastery in SwiftUI


Protocols

Protocols As a Type
class StudentClass: PersonProtocol {
var firstName: String This class also conforms to the
var lastName: String PersonProtocol on the previous page and
implements the getFullName function a little
init(first: String, last: String) {
firstName = first differently.
lastName = last
}

func getFullName() -> String {


return lastName + ", " + firstName
}
}
Notice the type for these properties is simply
struct Protocol_AsType: View { the protocol. The properties can be assigned to
var developer: PersonProtocol
var student: PersonProtocol any value as long as that class or struct
conforms to this protocol.
var body: some View {
VStack(spacing: 20) {
Use width: 214 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!")

Text(developer.getFullName())
Text(student.getFullName()) One is a struct and the other is a class. It
} doesn t matter as long as they conform to
.font(.title)
} 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 27 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 Subscriber {
protocol Publisher { protocol Publisher { func receive(subscription:)
func receive(subscriber:) func receive(subscriber:) func receive(input:)
} } func receive(completion:)
}

Publishers (and operators) have a receive function that allows them to The Subscriber protocol has 3 receive functions.
connect to subscribers. Let s talk about these on the next page…

www.bigmountainstudio.com 28 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 2 3
func func receive(input:) func receive(completion:)
receive(subscription:)

OK, we got your subscription Your water service is now


Yay! I m getting water!
and you can now get water. complete and we turned it off.

www.bigmountainstudio.com 29 Combine Mastery in SwiftUI



Protocols

Publisher & Subscriber Protocols

protocol Publisher { protocol Subscriber {


func receive(subscriber:) func receive(subscription:)
} func receive(input:)
func receive(completion:)
}

The goal here is to give you an understanding of how


protocols work and introduce you to the two major protocols You may have noticed I m not
behind Combine. Yes, these two protocols are implemented showing you the types for these
by all of the publishers, operators, and subscribers you will be ? functions yet.
working with.
They are set up in a way to allow the
You WILL NOT have to conform to these protocols yourself. developer to provide different types.
The Combine team did all of this for you! These protocols
make sure you can connect all publishers, operators, and This is allowed through the use of
subscribers together like pipes in a plumbing system. “generics” in the Swift language.

Most likely you will never have to create a class that conforms Let s learn more about how that
to these protocols in your career with Combine. works in the next section…

www.bigmountainstudio.com 30 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 The <T> is called a “type placeholder”. This
@State private var ageText = "" indicates a generic is being used and you
can substitute T with any type you want.
func getAgeText<T>(value1: T) -> String {
return String("Age is \(value1)")
}
// func getAgeText(value1: Int) -> String {
// return String("Age is \(value1)")
// } That one generic function can now replace
// func getAgeText(value1: String) -> String { these two functions.
// return String("Age is \(value1)")
// }

var body: some View {


VStack(spacing: 20) {
HeaderView("Generics",
subtitle: "Introduction",
Use width: 214 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 {
ageText = getAgeText(value1: 28) Because the parameter
} else { type is generic, you can
ageText = getAgeText(value1: "28") pass in any type.
}
}
Text(ageText)
}
.padding(.horizontal)
}
.font(.title)
}
}

www.bigmountainstudio.com 32 Combine Mastery in SwiftUI


Generics

Generics On Objects
struct Generic_Objects: View {
The generic (<T>) is declared on the class
class MyGenericClass<T> { so now the scope extends to all members
var myProperty: T within this class.

init(myProperty: T) {
self.myProperty = myProperty
}
}

var body: some View { You can initialize


let myGenericWithString = MyGenericClass(myProperty: "Mark") the class with
let myGenericWithBool = MyGenericClass(myProperty: true) different types.
Use width: 214 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)
}
.font(.title) 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).

www.bigmountainstudio.com 33 Combine Mastery in SwiftUI



Generics

Multiple Generics
struct Generic_Multiple: View {
Keep adding additional letters or names
class MyGenericClass<T, U> { separated by commas for your generic
var property1: T placeholders like this.
var property2: U

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 34 Combine Mastery in SwiftUI


Generics

Generics - Constraints
struct Generics_Constraints: View {
private var age1 = 25 You can specify your
private var age2 = 45 constraint the same way you
specify a parameter s type.
func getOldest<T: SignedInteger>(age1: T, age2: T) -> String {
if age1 > age2 {
return "The first is older." SignedInteger is a protocol
} else if age1 == age2 { adopted by Int, Int8, Int16,
return "The ages are equal" Int32, and Int64. So T can
} be any of those types.
return "The second is older."
}

var body: some View {


VStack(spacing: 20) {
HeaderView("Generics",
Use width: 214 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'.")

HStack(spacing: 40) { Don t worry, Xcode will tell you if


Text("Age One: \(age1)") the constraint you want to use
Text("Age Two: \(age2)") will work or not.
}

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 35 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 36 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


}
You use associatedtype to indicate it can
be any type.
struct FootballGame: GameScore {
You use typealias to declare the type when
typealias TeamScore = Int 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."
} The calculateWinner function will use whatever
return "Team two wins"
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 38 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 39 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
func calculateWinner(teamOne: Int, teamTwo: Int) -> String { types in protocols, you don t always have to use them.
if teamOne > teamTwo {
You could explicitly set the type where it is used, like in the
return "Team one wins"
calculateWinner function signature here.
} else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
}

www.bigmountainstudio.com 40 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 {
if teamOne > teamTwo { Like generics, you can also set type constraints so the
developer only uses a certain category of types that, for
return "Team one wins"
example, match a certain protocol.
}else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
}

www.bigmountainstudio.com 41 Combine Mastery in SwiftUI


associatedtype & typealias

Constraints
protocol Teams {
// This can be any type of collection, such as: Dictionary, Range, Set
associatedtype Team: Collection
The way you define a type constraint is the
var team1: Team { get set } same format you use for variables or even
generic constraints by using the colon followed
var team2: Team { get set }
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 {
Notice in this example I m not using a type
alias to define what type Team is.
return "Team 1 has more players"
} else if team1.count == team2.count {
Instead, I m explicitly using a string array
return "Both teams are the same size" which Swift will understand and set the type
} for me.
return "Team 2 has more players"
}
}

www.bigmountainstudio.com 42 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") {
comparison = weekendGame.compareTeamSizes() Use width: 214
}

Text(comparison)

Spacer()
}
.font(.title)
}
}

www.bigmountainstudio.com 43 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 Subscribers can receive any input from the connected publisher.
could be simple types like String, Int, or Bools or structs of data The Failure generic is constrained to the Error protocol.
you get from another data source.
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.

www.bigmountainstudio.com 44 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 { public protocol Subscriber {


associatedtype Output associatedtype Input
associatedtype Failure : Error 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 45 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 47 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 48 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) Subscriber (View)

@Published data Notify view of any changes View

www.bigmountainstudio.com 50 Combine Mastery in SwiftUI


@Published

Template

The code between the ObservableObject and View might look something like this:

Publisher (View Subscriber (View)

class MyViewModel: ObservableObject { struct Published_Intro: View {


@Published var data = “Some Data” Notify view of any changes @StateObject var vm = MyViewModel()
} }

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 51 Combine Mastery in SwiftUI




@Published

Introduction
class PublishedViewModel: ObservableObject {
@Published var state = "1. Begin State" After 1 second, the state
property is updated.
init() {
When an update happens,
// Change the name value after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
the observable object
self.state = "2. Second State" 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)
}
} 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.

www.bigmountainstudio.com 52 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 53 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"
}
}
}

The @Published property


struct Published_ReadWrite: View {
will get updated directly
@StateObject private var vm = PublishedViewModel()
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 54 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) { _, newValue in
message = newValue.isEmpty ? "❌ " : "✅ "
})
Text(message)
}
.padding() If we were to move this validation logic into the view
} model, how would we do it?
.font(.title) We can use Combine to handle this for us. Let s
} create your first Combine pipeline!
}

www.bigmountainstudio.com 55 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 = "" The validation result will be
@Published var validation: String = "" assigned to this property.

init() {
// Create pipeline here
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 57 Combine Mastery in SwiftUI




Your First Pipeline

The Pieces
Your pipeline always starts with a
publisher and always ends with a
subscriber.

Publisher Operator Subscriber


The subscriber is what requests the
The publisher sends out data. But data is
The operator is where you put logic to do data.
only sent out if someone wants it.
something to the data flowing through
A house that subscribes to water
the pipeline.
Just like a water tower, if no one is requests it to wash dishes, provide baths,
subscribing to water service then that etc.
This is where you can evaluate, modify
water will just sit there and not flow
and somehow affect the data and its flow. You ll be happy to know there are only a
through the pipeline.
few subscribers in Combine.

www.bigmountainstudio.com 58 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 = "" The name property is a String
@Published var validation: String = "" type.

init() { But $name is of type Publisher.


// Create pipeline here

$name
So what is this
} Publisher?
}
Apple says it is: “A publisher for
properties marked with the
@Published attribute.”

To you, that means it can be the


start of a Combine pipeline and
(Hold down OPTION and click on $name to get this quick help to pop up.)
can send values down that
pipeline.

Published<String> is the type of this @Published property In this case, it will send down a
here. This means a String is sent down the pipeline. String.

www.bigmountainstudio.com 59 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
init() { comes through this pipeline.
// 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
return "❌ " values can be published.
} else {
In a few pages, I m going to show you some alternative ways in which
return "✅ "
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.
})
}
}

OK, we have a publisher and an operator. We still need the third piece, the subscriber.

www.bigmountainstudio.com 60 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 61 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 Shorter Shortest


.map { name in
.map({ (name) in .map { $0.isEmpty ? "❌ " : “✅ " }
if name.isEmpty { return name.isEmpty ? "❌ " : "✅ "
}
return "❌ "
} else {
return "✅ "
} • You can remove the first set of • In a recent version of Swift, the return
}) parentheses. If the last (or only) keyword was made optional if you only
parameter is a closure, then we don t had one line of code in your function/
need the parentheses. This is called a closure. This is called an “implicit
“trailing closure”. return”.
• You don t need the parentheses around • The $0 notation can be used in place of
the value that is being passed into the the first parameter that is passed into
closure either. Xcode will add it the closure. These are called
automatically but you can remove it. “anonymous closure arguments” or
• Instead of using if then, you can use a “shorthand argument names”. More
Ternary operator ( Condition ? True info here.
part : False part ). • The braces don t have to be on
separate lines. This is a choice the
developer can make.

www.bigmountainstudio.com 62 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.

class YourFirstPipelineViewModel: ObservableObject { Assign Subscriber


@Published var name: String = ""
@Published var validation: String = "" The assign(to: )
subscriber will take the data
init() { coming down the pipeline and
// Create pipeline here just drop it right into the
$name @Published property you have
specified.
.map { $0.isEmpty ? "❌ " : "✅ " }
value
.assign(to: &$validation)
Yeah, it really is that easy. 😃
}
}

@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 63 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 The @Published property wrapper turns the property into a
value. It is considered a constant. 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 let vm = YourFirstPipelineViewModel()
means the parameter can be updated after the function has run:
let name: String = vm.name

func doubleThis(value: inout Int) { The ampersand is an indication But if you want access to the Publisher itself, you will have to use
value = value * 2 that says: the dollar sign like this:
}

var y = 4 Hey, this function


doubleThis(value: &y) let namePublisher = vm.$name
can and probably will
change the value that
you are passing in It might make more sense if I include the type:
here.

let namePublisher: Published<String>.Publisher = vm.$name

www.bigmountainstudio.com 64 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!"


message The Property
print(vm.message)

$message

(Playgrounds output)
The Publisher

www.bigmountainstudio.com 65 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 66 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.)

This subscriber does not allow you to


But you might notice further in the documentation that Apple also calls this function an cancel because it actually does it for
you! Very handy.

Let me get that for you.

www.bigmountainstudio.com 67 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:

class ViewModel: ObservableObject {


Play this video clip and watch what happens:
@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 68 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 Subscriber
You learned you could You learned about your You learned about the
use @Published first operator: map. assign subscriber
properties as Publishers which will take data
to create your pipelines. The map function coming down the
accepts values coming pipeline and assign it to @Published
You access the down the pipeline and a property. Property
Publisher part of the can evaluate and run
@Published property logic on them. This particular function
by using the dollar sign can ONLY work with
($). When it s done, it sends @Published properties.
the new value
downstream through the
pipeline.

www.bigmountainstudio.com 69 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.

The sink subscriber has a convenient way


of stopping the flow of data.
We call it a “handle”. Apple calls it a
“cancel” function.

You can do anything you


want once the data is in

Data
your sink.
Data

ta
Da

Da
ta Data

Let s see what this looks like in code…

www.bigmountainstudio.com 71 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 var cancellable: AnyCancellable?
$name Import Combine
.map { $0.isEmpty ? "❌ " : "✅ " } From this point on, you will init() {
need to import Combine for cancellable = $name
.assign(to: &$validation)
} all of your view models. .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 public protocol Cancellable {
one function, cancel(). func cancel()
}

www.bigmountainstudio.com 72 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

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 init() {
warning then use the underscore like this: _ = $name
.map { $0.isEmpty ? "❌ " : "✅ " }
(The underscore just means you are not using the result of the .sink { [unowned self] value in
function.) self.validation = value
}
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 73 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 = ""
On the previous page, the cancellable
vm.cancellable?.cancel()
property was public. We can access it directly
}
to call the cancel function to cancel the
}
validation subscription.
.font(.title)
} You may want to keep your cancellable
} private and instead expose a public
When you play this video, notice that
function you can call. See next page for an
after cancelling the subscription, the
example of this…
validation no longer happens.

www.bigmountainstudio.com 74 Combine Mastery in SwiftUI


􀎷
Your First Cancellable Pipeline

Long-Running Process - View Model


class LongRunningProcessViewModel: ObservableObject {
@Published var data = "Start Data"
@Published var status = ""
In this view model, the cancellable
private var cancellablePipeline: AnyCancellable?
property is private.

init() {
cancellablePipeline = $data
.map { [unowned self] value -> String in
status = "Processing..." Note: I m using the delay operator to
return value simulate a process that might take a long
} time.
.delay(for: 5, scheduler: RunLoop.main) I specified a 5-second delay (the for
.sink { [unowned self] value in parameter).
status = "Finished Process" 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() The cancelling functionality is now in a public cancel
// OR function that the view can call.
cancellablePipeline = nil
}
}

www.bigmountainstudio.com 75 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") {
vm.refreshData()
Use width: 214
}

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 76 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


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 cancellable ViewModel.status
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
This is a circular reference. the ViewModel.
Make this reference weak or
One of these objects cannot deinititialize unowned to prevent a circular
(be removed from memory) until the other reference.
one is removed first…UNLESS you make
one of the references weak or unowned.
www.bigmountainstudio.com 77 Combine Mastery in SwiftUI

Your First Cancellable Pipeline

Pipeline Lifecycle
Is [unowned self] better than [weak self]?

Unowned class LongRunningProcessViewModel: ObservableObject { 1 Class is removed from memory.


In this case, you can use [unowned @Published var data = "Start Data"
self]because when the ViewModel @Published var status = ""
class is de-initialized, the private var cancellablePipeline: AnyCancellable? 2 Cancellables are removed from
cancellablePipeline property will also memory. All pipelines are cancelled.
cancel and de-initialize which will Data can no longer be sent down the
destroy the related subscriber. init() { pipelines.
cancellablePipeline = $data
This means the sink s closure will no
.map { [unowned self] value -> String in
longer run. 3
status = "Processing..."
This is true for the scenario we have return value
Anything that was running within the
here where the sink is referencing } closures stopped when the
something within the same class (view
.delay(for: 5, scheduler: RunLoop.main) cancellables were cancelled and
model).
destroyed.
.sink { [unowned self] value in
Weak status = "Finished Process"
3
}

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.

www.bigmountainstudio.com 78 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 80 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.
private var validationCancellables: Set<AnyCancellable> = []
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.
init() {
$firstName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.firstNameValidation = value
} The sink subscriber returns an AnyCancellable but instead of
.store(in: &validationCancellables) 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
$lastName to a set of AnyCancellable types.
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.lastNameValidation = value
}
.store(in: &validationCancellables)
}
}

www.bigmountainstudio.com 81 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() Once the validation pipelines are cancelled,
} the validations no longer take place.
}
.font(.title)
} Let s see what
} vm.cancelAllValidations() is actually
doing.

www.bigmountainstudio.com 82 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
$lastName
reference, a pipeline no longer has a place
.map { $0.isEmpty ? "❌ " : "✅ " }
in memory and will become deallocated.
.sink { [unowned self] value in
self.lastNameValidation = value
}
This means that the subscription (sink) is
.store(in: &validationCancellables) 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 83 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 Many apps get images or data There is probably an operator You learned about one
@Published properties as from a URL. The data received is for everything you do today subscriber and I m sure you will
publishers. in JSON format and needs to be when handling data. use this one a lot. But
converted into a more usable sometimes your pipeline will
Did you know there are even format for your app. Explore the available operators handle data that doesn t end by
publishers built into some data and learn how to use them with being assigned to a @Published
types now? Learn how to do this easily with real SwiftUI examples. property.
Combine. Learn other options here.

Organizing Working with Handling Errors Debugging


Multiple Publishers
Your pipelines, from publisher to You will most likely want to Your pipelines aren t always
subscriber, don t always have to In plumbing, you need to catch and handle errors in your going to run perfectly when
be fully assembled when you connect multiple pipes together pipeline before using assign. you re constructing them.
use them. to deliver water to different
places or to merge hot and cold Learn how to use the catch Learn tips, tricks, and operators
Discover storing pieces of the water together. operator to return something to assist you in understanding
pipeline in functions or You can do the same thing in your app can work with. what is happening in your
properties later. Combine! pipeline.

www.bigmountainstudio.com 85 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)")
.foregroundStyle(vm.countColor)
} Combine is being used to produce the
.font(.title) 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.

www.bigmountainstudio.com 88 Combine Mastery in SwiftUI


􀎷
Publishers

@Published - View Model


class Published_IntroductionViewModel: ObservableObject {
var characterLimit = 30
@Published var data = ""
Use the dollar sign ($) to access the @Published
@Published var characterCount = 0
property s publisher. From here you can create a
@Published var countColor = Color.gray
pipeline so every time the property changes, this
pipeline will run.
init() {
When the data property changes I get the character
$data
count and assign it to another @Published
.map { data -> Int in
property.
return data.count
}
.assign(to: &$characterCount)
I also have a pipeline on the
$characterCount characterCount so when
.map { [unowned self] count -> Color in
Use width: 214
it changes, I figure out the
let eightyPercent = Int(Double(characterLimit) * 0.8) color to use for the text on
if (eightyPercent...characterLimit).contains(count) { the view.
return Color.yellow
} else if count > characterLimit {
return Color.red
}
return Color.gray
}
.assign(to: &$countColor)
}
}

www.bigmountainstudio.com 89 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 This is the error that could be sent to the subscriber if something goes
property (more specifically, the type wrong. Never means the subscriber should not expect an error/failure.
that will be sent to the subscriber). 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 91 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") {
The idea here is we want to make the text red if they
vm.selection.send("Lorenzo") select the same thing twice.
} But there is a problem that has to do with when a
Use width: 214
CurrentValueSubject s pipeline is run.
Button("Select Ellen") {
vm.selection.value = "Ellen" See view model on next page…
}

Text(vm.selection.value)
.foregroundStyle(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 92 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") {
“Calling send(_:) on a CurrentValueSubject also updates the current value,
vm.selection.value = "Ellen"
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 93 Combine Mastery in SwiftUI






Publishers

CurrentValueSubject - View Model


class CurrentValueSubjectViewModel: ObservableObject {
var selection = CurrentValueSubject<String, Never>("No Name Selected") Pipeline: Compares the previous value with the new
var selectionSame = CurrentValueSubject<Bool, Never>(false) value and returns true if they are the same.
var cancellables: [AnyCancellable] = []

init() {
This will NOT work.
selection
The newValue will ALWAYS equal
.map{ [unowned self] newValue -> Bool in
the current value.
if newValue == selection.value {
Unlike @Published properties,
return true this pipeline runs AFTER the
} else { current value has been set.
return false
}
}
.sink { [unowned self] value in Note: This whole if block could be shortened
selectionSame.value = value to just:
newValue == selection
objectWillChange.send()
}
.store(in: &cancellables)
}
} 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.

www.bigmountainstudio.com 94 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 3 The UI is automatically notified of


objectWillChange.send()) 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 95 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") {
The view model for this view is using a
vm.selection = "Lorenzo"
@Published property for just the
}
selection property.

Button("Select Ellen") { So you will notice we set it normally here.


vm.selection = "Ellen"
}

Text(vm.selection)
.foregroundStyle(vm.selectionSame.value ? .red : .green)
}
.font(.title)
}
}

www.bigmountainstudio.com 96 Combine Mastery in SwiftUI


􀎷
Publishers

CurrentValueSubject Compared - View Model


class CurrentValueSubject_ComparedViewModel: ObservableObject {
The only thing that has changed is the selection
@Published var selection = "No Name Selected"
property is now using the @Published property
var selectionSame = CurrentValueSubject<Bool, Never>(false)
wrapper instead of being a CurrentValueSubject
var cancellables: [AnyCancellable] = []
publisher.

init() {
$selection
.map{ [unowned self] newValue -> Bool in
if newValue == selection { This will work now!
return true The selection property will still have the PREVIOUS value.
} else {
return false Remember the sequence for @Published properties:
}
1. The pipeline is run
}
2. The value is set
.sink { [unowned self] value in
3. The UI is automatically notified of changes
selectionSame.value = value
objectWillChange.send()
So the selection property is only updated AFTER the pipeline has
} run first which allows us to inspect the previous value.
.store(in: &cancellables)
}
}

You still need objectWillChange.send()


because the value is still being assigned to a
CurrentValueSubject.

www.bigmountainstudio.com 97 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 99 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
The tryMap operator gives you a closure to run some code for each item that
.tryMap{ item in
comes through the pipeline with the option of also throwing an error.
if item == "🧨 " {

throw BombDetectedError()

}
In this example, the Empty publisher is used to end a pipeline immediately after an
return item error is caught. The catch operator is used to intercept errors and supply another
} publisher.

.catch { (error) in
Note: I didn t have to explicitly set the completeImmediately parameter to true
Empty(completeImmediately: true) because that is the default value.
}

.sink { [unowned self] (item) in

dataToView.append(item)

www.bigmountainstudio.com 100 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()
When you tap Save, a save function on the
Button("Save") { view model is called. The age is validated and
vm.save(age: Int(age) ?? -1) 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 102 Combine Mastery in SwiftUI


Publishers

Fail - View Model


class Validators {
static func validAgePublisher(age: Int) -> AnyPublisher<Int, InvalidAgeError> {
This function can return different publisher
if age < 0 {
return Fail(error: InvalidAgeError.lessThanZero) types. Luckily, we can use
.eraseToAnyPublisher() eraseToAnyPublisher to make them all a
} else if age > 100 { common type of publisher that returns an Int
return Fail(error: InvalidAgeError.moreThanOneHundred) or an InvalidAgeError as its failure type.
.eraseToAnyPublisher()
Learn more about AnyPublisher and
}
organizing pipelines.
return Just(age)
.setFailureType(to: InvalidAgeError.self)
.eraseToAnyPublisher()
} 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
class Fail_IntroViewModel: ObservableObject { publisher are all the same type that we return from this function.
@Published var age = 0
@Published var error: InvalidAgeError?

func save(age: Int) {


_ = Validators.validAgePublisher(age: age) If validAgePublisher returns a
.sink { [unowned self] completion in Fail publisher then the sink
if case .failure(let error) = completion {
completion will catch it and the error
self.error = error
is assigned to the error @Published
}
} receiveValue: { [unowned self] age in property.
self.age = age
Learn more about error-throwing
} Or else the Just publisher is returned
and non-error-throwing pipelines
} and the age is used.
} in the Handling Errors chapter.

www.bigmountainstudio.com 103 Combine Mastery in SwiftUI



Publishers

Fail - InvalidAgeError Enum


enum InvalidAgeError: String, Error, Identifiable {
var id: String { rawValue }
We re using an enum to represent the error
case lessThanZero = "Cannot be less than zero"
when we know the possible error states.
case moreThanOneHundred = "Cannot be more than 100"
In this case we have two error states when the
}
value is:
1. Less than zero
2. More that 100

In the SwiftUI, we use an alert to show the


.alert(item: $vm.error) { error in error.
Alert(title: Text("Invalid Age"), message: Text(error.rawValue)) When the observable object s error property
} changes from nil, this alert will show.

We are using the rawValue for the message.

The rawValue is the string that is assigned to


each enum case.

www.bigmountainstudio.com 104 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>

This is the error that could be sent to the subscriber if something


The type you want to pass down the
goes wrong. Never means the subscriber should not expect an error/
pipeline in the future to the
failure. Otherwise, you can create your own custom error and set this
subscriber.
type.

let futurePublisher = Future<String, Never> { promise in What is Result?


Result is an enum with two
promise(Result.success("👋 ")) cases: success and failure.

} The promise parameter passed into the closure is You can assign a value to each
actually a function definition. The function looks like one. The value is a generic so
you can assign a String, Bool,
this: Int, or any other type to them.

promise(Result<String, Never>) -> Void In this example, a String is


being assigned to the success
You want to call this function at some point in the case.
future s closure.

(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 106 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.
.font(.title)
The sayGoodbye function will be delayed
}
before returning a value.
}

www.bigmountainstudio.com 107 Combine Mastery in SwiftUI


Publishers

Future - View Model


class Future_IntroViewModel: ObservableObject {
@Published var hello = ""
In this example, a new Future publisher is being
@Published var goodbye = ""
created and returning one value, “Hello, World!”.

var goodbyeCancellable: AnyCancellable?

func sayHello() { Because Future is declared with no possible failure (Never), this becomes a non-
Future<String, Never> { promise in error-throwing pipeline.
promise(Result.success("Hello, World!")) We don t need sink(receiveCompletion:receiveValue:) to look for and handle
} errors. So, assign(to:) can be used.
.assign(to: &$hello)
} (See chapter on Handling Errors to learn more.)

func sayGoodbye() {
let futurePublisher = Future<String, Never> { promise in Here is an example of where the Future publisher is being assigned to a
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
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.)
promise(.success("Goodbye, my friend 👋 "))
}
}
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.)
goodbyeCancellable = futurePublisher
Also, there are two reasons why this pipeline is being assigned to an AnyCancellable:
.sink { [unowned self] message in
1. Because there is a delay within the future s closure, the pipeline will get deallocated as
goodbye = message 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.
}

www.bigmountainstudio.com 108 Combine Mastery in SwiftUI





Publishers

Future - Immediate Execution


class Future_ImmediateExecutionViewModel: ObservableObject { This is the view model.
@Published var data = ""

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()

Use width: 214 var body: some View {


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
.onAppear { is simply to demonstrate that the Future publisher will
vm.fetch() publish immediately, whether it has a subscriber or not.
}
} I m pretty sure Apple doesn t intend it to be used this way.
}

www.bigmountainstudio.com 109 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") { No matter how many times you tap this button,
vm.runAgain() 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.
vm.fetch() When the “Run Again” button is tapped, the
} same future is reused.
}
}

www.bigmountainstudio.com 110 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)
}
}

So what if you don t want the Future publisher to execute


This function can be run repeatedly and the
immediately when created? What can you do?
futurePublisher will emit the same, original value,
We look at wrapping a Future with another publisher to help
every single time but will not actually get executed.
with this on the next page.

www.bigmountainstudio.com 111 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) The word “defer” means to “postpone some activity


Use width: 214 or event to a later time”. In this case, putting off
Button("Run Again") { executing the Future until it is needed.
vm.runAgain()
}
Using the Deferred publisher, the Future publisher
Text(vm.secondResult) will execute every time this button is tapped.
}
.font(.title)
.onAppear {
vm.fetch()
}
} 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.

www.bigmountainstudio.com 112 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
let futurePublisher = Deferred { publisher within it like this.
Future<String, Never> { promise in
promise(.success("Future Publisher has run! 🙌 ")) 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
print("Future Publisher has run! 🙌 ") time a subscriber is attached.
}
}

func fetch() {
futurePublisher
.assign(to: &$firstResult)
}

func runAgain() {
futurePublisher
.assign(to: &$secondResult)
}
}

Note: I am not sure what else to use the Deferred publisher


This function can be run repeatedly and the with because the Future publisher is the only one I know that
futurePublisher will now get executed every time. executes immediately. All the other publishers I know of do
not publish unless a subscriber is attached.

www.bigmountainstudio.com 113 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.

Deferred
newApiPublisher =
Future

Successful Operation

promise(.success(<Some Type>))

Failed Operation

promise(.failure(<Some Error>))

www.bigmountainstudio.com 114 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 {
In this example, the Just publisher is being used to
vm.fetch()
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 116 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)
}
You can see in this chapter that Apple added
if dataIn.count > 0 { built-in publishers to many existing types. For
everything else, there is Just.
Just(dataIn[0])
.map { item in It may not seem like a lot but being able to start
item.uppercased() a pipeline quickly and easily this way opens the
} door to all the operators you can apply to the
pipeline.
.assign(to: &$data)
} After Just publishes the one item, it will finish
} the pipeline.
}

www.bigmountainstudio.com 117 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")
.foregroundStyle(.green)
case .invalid:
Image(systemName: "x.circle.fill") A PassthroughSubject is a good
.foregroundStyle(.red)
default: candidate when you need to send a
EmptyView() value through a pipeline but don t
} necessarily need to hold on to that
}
} value.
.padding()
I use it here to validate a value when a
Button("Verify CC Number") {
button is tapped.
vm.verifyCreditCard.send(vm.creditCard)
}
}
.font(.title)
} Like the CurrentValueSubject, you
}
have access to a send function that will
send the value through your pipeline.

www.bigmountainstudio.com 119 Combine Mastery in SwiftUI


􀎷

Publishers

PassthroughSubject - View Model


enum CreditCardStatus {
The UI shows an Pipeline: The idea here is that a credit card number is
case ok
image based on checked to see if it s 16 digits. The status property is
case invalid
the credit card updated with the result.
case notEvaluated status.
}

class PassthroughSubjectViewModel: ObservableObject { This Passthrough publisher will not


@Published var creditCard = "" retain a value. It simply expects a
@Published var status = CreditCardStatus.notEvaluated String.
let verifyCreditCard = PassthroughSubject<String, Never>()
If there is a subscriber attached to it
init() { then it will send any received values
through the pipeline to the subscriber.
verifyCreditCard
.map{ creditCard -> CreditCardStatus in
if creditCard.count == 16 {
return CreditCardStatus.ok
} else { The verifyCreditCard publisher is specified to receive a
return CreditCardStatus.invalid String and not return any error:
PassthroughSubject<String, Never>
}
}
Without doing anything, the pipeline expects a String will go
.assign(to: &$status)
all the way through. But you can change this.
}
}
And that s what is happening here. The map operator now
returns an enum CreditCardStatus and we store the result
Remember, when using the assign(to:) subscriber, there is no in the status property.
need to store a reference to this pipeline (AnyCancellable).

www.bigmountainstudio.com 120 Combine Mastery in SwiftUI




Sequence
[Item10
,
Item9,
Item8,
e m
It

Item Item Item Item

There are types in Swift that 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 122 Combine Mastery in SwiftUI


Publishers

Sequence - View Model


class SequenceIntroViewModel: ObservableObject {
@Published var dataToView: [String] = [] The publisher property on the array is the
var cancellables: Set<AnyCancellable> = []
Sequence publisher.

func fetch() {
var dataIn = ["Paul", "Lem", "Scott", "Chris", "Kaya", "Mark", "Adam", "Jared"]

// Process values
dataIn.publisher
.sink(receiveCompletion: { (completion) in
print(completion)
}, receiveValue: { [unowned self] datum in
self.dataToView.append(datum)
print(datum)
})
(Xcode Debugger Console)
.store(in: &cancellables)

// These values will NOT go through the pipeline. Notice if you try to add more to the sequence
later, the pipeline will not execute.
// The pipeline finishes after publishing the initial set.
dataIn.append(contentsOf: ["Rod", "Sean", "Karin"])
As soon as the initial sequence was published, it
} was automatically finished as you can see with
} the print statement in the receiveCompletion
closure.

www.bigmountainstudio.com 123 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 124 Combine Mastery in SwiftUI


Publishers

Sequence - With String


class Sequence_StringViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellables: Set<AnyCancellable> = []

func fetch() { If you need to iterate over


let dataIn = "Hello, World!" each character in a String,
dataIn.publisher
you can use its publisher
.sink { [unowned self] datum in
self.dataToView.append(String(datum)) property.
print(datum)
}
.store(in: &cancellables)
}
}

struct Sequence_String: View {


@StateObject private var vm = Sequence_StringViewModel()
Use width: 214
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 125 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 127 Combine Mastery in SwiftUI


􀎷
Publishers

Timer - View Model


class TimerIntroViewModel: ObservableObject {
@Published var data: [String] = [] I created another pipeline on the interval
@Published var interval: Double = 1 published property so when it changes
value, I can restart the timer s pipeline so it
private var timerCancellable: AnyCancellable? reinitializes with the new interval value.
private var intervalCancellable: AnyCancellable?

let timeFormatter = DateFormatter()


Learn more
init() { about dropFirst
timeFormatter.dateFormat = "HH:mm:ss.SSS"

intervalCancellable = $interval
.dropFirst() You set the Timer s interval
.sink { [unowned self] interval in with the publish modifier.
// Restart the timer pipeline
timerCancellable?.cancel() For the on parameter, I Use width: 214
data.removeAll()
set .main to have this run on
start()
} the main thread.
}
The last parameter is the
func start() { RunLoop mode.
timerCancellable = Timer
(Run loops manage events and
.publish(every: interval, on: .main, in: .common)
.autoconnect()
work and allow multiple things
.sink{ [unowned self] (datum) in to happen simultaneously.)
data.append(timeFormatter.string(from: datum)) In almost all cases you will just
} use the common run loop.
}
}

The autoconnect operator seen here allows the Timer to automatically start publishing items.

www.bigmountainstudio.com 128 Combine Mastery in SwiftUI




Publishers

Timer Connect - View


struct Timer_Connect: View {
@StateObject private var vm = Timer_ConnectViewModel() On the previous page, you saw that
the autoconnect operator allowed
var body: some View { the Timer to publish data right away.
VStack(spacing: 20) { 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 { In this example, when the Connect


Button("Connect") { vm.start() } button is tapped it will call the
.frame(maxWidth: .infinity) connect function manually and allow
Button("Stop") { vm.stop() } 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 129 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
init() {
function will only work on the
timeFormatter.dateFormat = "HH:mm:ss.SSS" publisher itself.
timerPublisher
.sink { [unowned self] (datum) in
data.append(timeFormatter.string(from: datum))
} When the connect function is called, Use width: 214
.store(in: &cancellables) the Timer will start to publish.
}
Note: The connect function ONLY
works on the publisher itself. So you
func start() {
will have to separate your subscriber
timerCancellable = timerPublisher.connect()
from your publisher as you see here.
}

func stop() {
timerCancellable?.cancel() The connect and autoconnect functions
data.removeAll()
are only available on publishers that conform
to the ConnectablePublisher protocol, like
}
the Timer.
}

www.bigmountainstudio.com 130 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)

Download task
URLSession
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 132 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


URLSession.shared
let session = URLSession(configuration: configuration)

• Great for simple tasks like fetching data from a URL to • You can change the default request and response timeouts
memory • You can make the session wait for connectivity to be
• You can t obtain data incrementally as it arrives from the established
server • You can prevent your app from using a cellular network
• You can t customize the connection behavior • Add additional HTTP headers to all requests
• Your ability to perform authentication is limited • Set cookie, security, and caching policies
• You can t perform background downloads or uploads when • Support background transfers
your app isn t running • See more options here.
• You can t customize caching, cookie storage, or credential
storage

For the examples in this book, I will just be using URLSession.shared.

www.bigmountainstudio.com 133 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 Error

The data is what is returned from The response is like the status of If there was some problem with
the URL you provided to the how the call to the URL went. Could trying to connect and get data then
DataTaskPublisher. Note: What is it connect? Was it successful? What an error is thrown.
returned is represented as bytes in kind of data was returned?
memory, not text or an image.

www.bigmountainstudio.com 134 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 135 Combine Mastery in SwiftUI




Publishers

DataTaskPublisher - View Model


struct CatFact: Decodable {
Many more fields are Note: In order to keep this first example as simple as
let _id: String
returned from the API but possible, there are a lot of things I m NOT doing, such as
let text: String
we only care about two. checking for and handling errors. I ll cover this in the
}
following pages.

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 Response Error
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [unowned self] catFact in
dataToView = catFact The Data and Response can be inspected inside a map operator. Since
}) dataTaskPublisher returns these two things, the map operator will
.store(in: &cancellables) automatically expose those two things as input parameters.
}
} If dataTaskPublisher throws an error then it ll go straight to the sink s
completion handler.

www.bigmountainstudio.com 136 Combine Mastery in SwiftUI






Publishers

DataTaskPublisher - Map
struct CatFact: Decodable {
let _id: String The dataTaskPublisher publishes a tuple: Data & URLResponse.
let text: String (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.

class UrlDataTaskPublisher_IntroViewModel: ObservableObject {


Map
@Published var dataToView: [CatFact] = [] And that is what we are doing with the map operator. The map receives
var cancellables: Set<AnyCancellable> = [] 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
func fetch() { one thing being returned. You could use return data if it makes it
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! more clear for you.)
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
Can it be shorter?
Yes! I wanted to start with this format so you can explicitly see
data
the tuple coming in from the dataTaskPublisher.
}
.decode(type: [CatFact].self, decoder: JSONDecoder()) To make this shorter you can use what s called “shorthand
.receive(on: RunLoop.main) argument names” or “anonymous closure arguments”. It s a
.sink(receiveCompletion: { completion in way to reference arguments coming into a closure with a dollar
print(completion) sign and numbers:
}, 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:
}
.map { $0.data }

www.bigmountainstudio.com 137 Combine Mastery in SwiftUI




Publishers

DataTaskPublisher - Decode
struct CatFact: Decodable {
let _id: String
The map operator is now republishing just the data value we
let text: String received from dataTaskPublisher.
}
What is Data?
class UrlDataTaskPublisher_IntroViewModel: ObservableObject { The data value represents what we received from the URL
endpoint. It is just a bunch of bytes in memory that could
@Published var dataToView: [CatFact] = []
represent different things like text or an image. In order to
var cancellables: Set<AnyCancellable> = []
use data, we will have to transform or decode it into
something else.
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! Decode
URLSession.shared.dataTaskPublisher(for: url) The decode operator not only decodes those bytes into
something we can use but will also apply the decoded data
.map { $0.data }
into a type that you specify.
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
Since you know you are getting back JSON (Javascript
Object Notation) from the URL endpoint, you can use the
.sink(receiveCompletion: { completion in JSONDecoder.
print(completion)
We also know there is a “_id” field and a “text” field in that
}, receiveValue: { [unowned self] catFact in
JSON so we create a struct containing those two fields. In
dataToView = catFact order for the decode operator to work, we have to make that
}) struct conform to Decodable.
.store(in: &cancellables) But notice we re not putting the data into one CatFact.
} We re putting the data into an array of CatFact objects.
}

www.bigmountainstudio.com 138 Combine Mastery in SwiftUI




Publishers

DataTaskPublisher - Receive(on: )
struct CatFact: Decodable {
let _id: String Asynchronous
The dataTaskPublisher will run asynchronously. This means that
let text: String
your app will be doing multiple things at one time.
}
While your app is getting data from a URL endpoint and
decoding it in the background, the user can still use your app
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
and it ll be responsive in the foreground.
@Published var dataToView: [CatFact] = []
var cancellables: Set<AnyCancellable> = [] 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.
func fetch() {
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! We call the background and foreground “threads” in memory.
URLSession.shared.dataTaskPublisher(for: url)
Thread Switching
.map { $0.data } To move data that is coming down your background pipeline to
.decode(type: [CatFact].self, decoder: JSONDecoder()) a new foreground pipeline, you can use the receive(on:)
.receive(on: RunLoop.main) operator.
.sink(receiveCompletion: { completion in It basically is saying, “We are going to receive this data coming
print(completion) down the pipeline on this new thread now.” See section on
}, receiveValue: { [unowned self] catFact in receive(on:).
dataToView = catFact
Scheduler
})
You need to specify a “Scheduler”. A scheduler specifies how
.store(in: &cancellables) 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.)

www.bigmountainstudio.com 139 Combine Mastery in SwiftUI





Publishers

DataTaskPublisher - Sink
struct CatFact: Decodable {
let _id: String
let text: String Sink
} There are two sink subscribers:
1. sink(receiveValue:)
2. sink(receiveCompletion:receiveValue:)
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
@Published var dataToView: [CatFact] = [] When it comes to this pipeline, we are forced to use the second
var cancellables: Set<AnyCancellable> = [] one because this pipeline can fail. Meaning the publisher and
other operators can throw an error.
func fetch() { In this pipeline, the dataTaskPublisher can throw an error
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! and the decode operator can throw an error.
URLSession.shared.dataTaskPublisher(for: url)
Xcode s autocomplete won t even show you the first sink
.map { $0.data }
option for this pipeline so you don t have to worry about which
.decode(type: [CatFact].self, decoder: JSONDecoder()) one to pick.
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in Handling Errors
There are many different ways you can handle errors that might
print(completion)
be thrown using operators or subscribers. For more information
}, receiveValue: { [unowned self] catFact in on options, look at the chapter Handling Errors.
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
.store(in: &cancellables) an alert on the view using another example on the next page.
}
}

www.bigmountainstudio.com 140 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)
One way the alert modifier
works is it can monitor a
}
@Published property. If
.font(.title)
that property becomes not
.onAppear {
nil then it will pass the value
vm.fetch() of that property into a
} closure and we use that
.alert(item: $vm.errorForAlert) { errorForAlert in value to create and present
Alert(title: Text(errorForAlert.title), 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 141 Combine Mastery in SwiftUI



Publishers

Handling Errors - View Model


struct ErrorForAlert: Error, Identifiable {
let id = UUID() Notice the ErrorForAlert conforms to Identifiable. This just means
let title = "Error"
you need to give it a property called “id” to conform to it.

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 142 Combine Mastery in SwiftUI


Publishers

class DataTaskPublisher_ErrorsViewModel: ObservableObject {


@Published var dataToView: [CatFact] = [] ?
@Published var errorForAlert: ErrorForAlert?
I changed the URL so that
var cancellables: Set<AnyCancellable> = [] 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
.map { (data: Data, response: URLResponse) in different from your traditional switch
data case control flow.
}
.decode(type: [CatFact].self, decoder: JSONDecoder()) This is a shorthand to examine just
one case of an enum that has an
.receive(on: RunLoop.main)
associated value like failure. This is
.sink(receiveCompletion: { [unowned self] completion in
because we re only interested when
if case .failure(let error) = completion { the completion is a failure.
errorForAlert = ErrorForAlert(message: "Details: \(error.localizedDescription)")
} Learn more about if case here.
}, receiveValue: { [unowned self] catFact in
dataToView = catFact
})
.store(in: &cancellables)
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.

www.bigmountainstudio.com 143 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 The server is thinking through the error. Throw Errors
responses When it comes to throwing errors from operators, you
want to look for operators that start with the word “try”.
2xx Success The request was successfully completed and the server gave This is a good indication that the operator will allow you
the browser the expected response. to throw an error and so skip all the other operators
between it and your subscriber.
3xx Redirection You got redirected somewhere else. The request was received,
but there’s a redirect of some kind. For example, if you wanted to throw an error from the map
operator, then use the tryMap operator instead.
4xx Client 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 Hide Errors
the website’s side of the conversation and often appears when You may not want to show any error at all to the user and
a page doesn’t exist on the site.) instead hide it and take some other action in response.

5xx Server errors Failure. A valid request was made by the client but the server For example, you could use the replaceError operator to
failed to complete the request. catch the error and then publish some default value
instead.

Source: https://moz.com/learn/seo/http-status-codes

www.bigmountainstudio.com 144 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),
In this example, the Big Mountain
message: Text(errorForAlert.message))
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.

www.bigmountainstudio.com 146 Combine Mastery in SwiftUI



Publishers

Getting an Image - View Model


class DataTaskPublisher_ForImagesViewModel: ObservableObject {
@Published var imageView: Image? Note: To understand all of these
@Published var errorForAlert: ErrorForAlert? parts better, I recommend looking at
the previous section of the
var cancellables: Set<AnyCancellable> = [] DataTaskPublisher.

func fetch() {
let url = URL(string: “https://d31ezp3r8jwmks.cloudfront.net/C3JrpZx1ggNrDXVtxNNcTz3t")!

URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
The tryMap operator is like map
.tryMap { data in except it allows you to throw an error.
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 Use width: 214
.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 {
if error is ErrorForAlert {
and the user will see it.
errorForAlert = (error as! ErrorForAlert)
} else {
errorForAlert = ErrorForAlert(message: "Details: \
(error.localizedDescription)")
}
}
}, receiveValue: { [unowned self] image in
imageView = image The sink s completion closure is
}) 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 147 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 148 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
var cancellables: Set<AnyCancellable> = [] a UIImage will fail.

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)
} If an error comes down the pipeline the
.replaceError(with: Image("blank.image")) replaceError operator will receive it
.receive(on: RunLoop.main) 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.
}
} 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.

www.bigmountainstudio.com 149 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 Applying Mathematical Applying Sequence Controlling Timing


to Elements Operations on Elements Operations to Elements
• Count
• Debounce
• AllSatisfy •Append
• Max • Delay(for:)
• Drop(untilOutputFrom:)
• TryAllSatisfy • Max(by:)
• TryMax(by:) • DropFirst • MeasureInterval
• Contains
• Min • Prefix
• Contains(where:) • Throttle
• Min(by:) • Prefix(untilOutputFrom:)
• TryContains(where:) • TryMin(by:) • Prepend • Timeout

Filtering Elements Mapping Reducing Elements Selecting Specific Specifying


• CompactMap • Collect Elements
Elements Schedulers
• TryCompactMap • First
• Map • Collect By Count • First(where:) • Overview
• Filter
• Collect By Time • TryFirst(where:)
• TryFilter • TryMap • Receive(on:)
• RemoveDuplicates • Collect By Time or Count • Last
• ReplaceNil • Last(where:) • Subscribe(on:)
• RemoveDuplicates(by:) • IgnoreOutput
• SetFailureType • TryLast(where:)
• TryRemoveDuplicates • Reduce • Output(at:)
• ReplaceEmpty • Scan • TryReduce • Output(in:)
• TryScan

www.bigmountainstudio.com 151 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()
The allFibonacciCheck will see if
resultVisible = true
} all numbers entered are in the
Fibonacci sequence.
Text(vm.allFibonacciNumbers ? "Yes" : "No")
.opacity(resultVisible ? 1 : 0)
} (A Fibonacci number is one that is the
.padding(.bottom) result of adding the previous two
.font(.title) numbers, starting with 0 and 1.
}
Example: 0,1,1,2,3,5,8,…)
}

www.bigmountainstudio.com 154 Combine Mastery in SwiftUI


􀎷
Operators

allSatisfy - View Model


class AllSatisfy_IntroViewModel: ObservableObject {

@Published var numbers: [Int] = [] The allSatisfy operator will check


each number in the numbers array to
@Published var allFibonacciNumbers = false
see if they are in the Fibonacci
sequence.
func allFibonacciCheck() {
If all are Fibonacci numbers, then true
let fibonacciNumbersTo144 = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
is assigned to allFibonacciNumbers
property and the pipeline finishes
numbers.publisher normally.

.allSatisfy { (number) in
But as soon as allSatisfy finds a
fibonacciNumbersTo144.contains(number) number that is not a Fibonacci
} number, then a false is published and
the pipeline finishes early.
.assign(to: &$allFibonacciNumbers)

func add(number: String) { Note: You may also notice that I m using Shorthand Argument Names
Here is an alternative way to write this using
if number.isEmpty { return } numbers.publisher here instead of $numbers.
shorthand argument names:
numbers.append(Int(number) ?? 0)
In this situation, $numbers will not work because its
.allSatisfy {
} type is an array, not an individual item in the array. fibonacciNumbersTo144.contains($0)
} }
By using numbers.publisher, I m actually using the
Sequence publisher so each item in the array will go
through the pipeline individually.

www.bigmountainstudio.com 155 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) The idea here is that when the
Button(action: { pipeline will return true if all numbers
are Fibonacci numbers but if any
vm.add(number: number)
number is over 144, an error is
number = ""
thrown and displayed as an alert.
}, label: { Image(systemName: "plus") })
} The view is continued on the next
.padding() page.

www.bigmountainstudio.com 157 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) Use width: 214
.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()
)
}
}
}

When invalidNumberError has a value, this alert will show.


This error will get set when a number above 144 is detected.

www.bigmountainstudio.com 158 Combine Mastery in SwiftUI


Operators

TryAllSatisfy - View Model


class TryAllSatisfy_IntroViewModel: ObservableObject {
struct InvalidNumberError: Error, Identifiable
@Published var numbers: [Int] = [] {
@Published var allFibonacciNumbers = false var id = UUID()
@Published var invalidNumberError: InvalidNumberError? }

func allFibonacciCheck() {
This is the custom Error object that will be thrown. It
let fibonacciNumbersTo144 = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144] also conforms to Identifiable so it can be used to
show an alert in the view.
_ = 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): If tryAllSatisfy detects a number over 144, an
self.invalidNumberError = error as? InvalidNumberError error is thrown and the pipeline will then finished
default: (completed).
break
} The subscriber (sink) receives the error in the
} receiveValue: { [unowned self] (result) in receivesCompletion closure.
allFibonacciNumbers = result
}
}

func add(number: String) {


if number.isEmpty { return }
numbers.append(Int(number) ?? 0)
}
}

www.bigmountainstudio.com 159 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 161 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
The prefix operator just returns
.prefix(2)
.sink { [unowned self] (item) in the first 2 items in this pipeline.
description += item + "\n"
}
.store(in: &cancellables)
These single-purpose publishers will just look for one match and
incomingData.publisher publish a true or false to the @Published properties.
.contains("Air conditioning")
.assign(to: &$airconditioning)
Remember, when the first match is found, the publisher will
incomingData.publisher finish, even if there are more items in the pipeline.
.contains("Heating")
.assign(to: &$heating)

incomingData.publisher
.contains("Basement")
Can I use contains on my custom data objects?
If they conform the Equatable protocol you can. The
.assign(to: &$basement)
} 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 162 Combine Mastery in SwiftUI


Contains(where: )

1==
12 12 12 true
2==

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 164 Combine Mastery in SwiftUI


Operators

Contains(where: ) - View Model


class Contains_WhereViewModel: ObservableObject {
@Published var fruitName = "" struct Fruit: Identifiable {
let id = UUID()
@Published var vitaminA = false
var name = ""
@Published var vitaminB = false
var nutritionalInformation = ""
@Published var vitaminC = false }

func fetch() {
let incomingData = [Fruit(name: "Apples", nutritionalInformation: "Vitamin A, Vitamin C")]

_ = incomingData.publisher Notice in this case I m not storing the cancellable in a


.sink { [unowned self] (fruit) in
property because I don t need to. After the pipeline
fruitName = fruit.name
} 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") These single-purpose publishers will just look for one
}) match and publish a true or false to the @Published
.assign(to: &$vitaminA)
properties.
incomingData.publisher
.contains(where: { (fruit) -> Bool in Remember, when the first match is found, the publisher
fruit.nutritionalInformation.contains("Vitamin B") will finish, even if there are more items in the pipeline.
})
.assign(to: &$vitaminB)

incomingData.publisher
.contains { (fruit) -> Bool in Notice how this contains(where: ) is written differently
fruit.nutritionalInformation.contains("Vitamin C") without the parentheses. This is another way to write
} the operator that the compiler will still understand.
.assign(to: &$vitaminC)
}
}

www.bigmountainstudio.com 165 Combine Mastery in SwiftUI





TryContains(where: )
error

2==
12 12 12 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
.pickerStyle(SegmentedPickerStyle()) is compared to all the items in the search
result to see if it exists or not.
Button("Search") {
vm.search()
}

Text("Result: \(vm.result)") If
} tryContains(where:)
.font(.title) throws an error, then this
.alert(item: $vm.invalidSelectionError) { alertData in alert will show.
Alert(title: Text("Invalid Selection"))
} See how on the next
}
page.
}

www.bigmountainstudio.com 167 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() If the user selected Mars then an error is thrown.
.tryContains(where: { [unowned self] (item) -> Bool in The condition for when the error is thrown can be
if place == "Mars" { anything you want.
throw InvalidSelectionError()
}
But if an item from your data source contains the
return item == place
}) place selected, then a true will be published and
.sink { [unowned self] (completion) in the pipeline will finish.
switch completion {
case .failure(let error):
self.invalidSelectionError = error as? InvalidSelectionError
default:
break
}
} receiveValue: { [unowned self] (result) in
self.result = result ? "Found" : "Not Found"
} Learn More
} • dropFirst
}

www.bigmountainstudio.com 168 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 {


NavigationStack {
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)")
})
}
Use width: 214 }
.font(.title)
.navigationTitle("Count")
.onAppear { vm.fetch() }
}
}
}

struct CountDetailView: View {


var data: [String]

var body: some View {


List(data, id: \.self) { datum in
Text(datum)
}
.font(.title)
}
}

www.bigmountainstudio.com 171 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
This is a very simplistic example of a
.count() very simple operator.
.assign(to: &$count)
Use width: 214
}
}

www.bigmountainstudio.com 172 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 { This view shows a collection of data and the
vm.fetch()
minimum values for strings and ints using the
}
}
max operator.
}

www.bigmountainstudio.com 174 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
Pretty simple operator. It will get the Finding the max value depends
.max()
max string or max int. on types conforming to the
.assign(to: &$maxValue)
Comparable protocol.

let dataInNumbers = [900, 245, 783] The Comparable protocol allows


numbers = dataInNumbers the Swift compiler to know how
dataInNumbers.publisher to order objects and which is
The maximum value is ONLY greater or lesser than others.
.max()
published once the publisher has
.assign(to: &$maxNumber)
sent all of the items through the But what if a type does not
} pipeline. 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 175 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 .foregroundStyle(.secondary)
}
In this view, each row is a Profile struct
with a name and city.
Text("Max City: \(vm.maxValue)")
And I m getting the maximum city (as a
.bold()
string).
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 177 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"),
Profile(name: "Rebecca", city: "Atlanta"), The max(by:) operator receives the current and next item
Profile(name: "Christina", city: "Stuttgart"), in the pipeline.
Profile(name: "Lorenzo", city: "Rome"), You can then define your criteria to get the max value.
Profile(name: "Oliver", city: "London")]
I should rephrase that. You re not exactly specifying the
profiles = dataIn criteria to get the max value, instead, you re specifying the
ORDER so that whichever item is last is the maximum.
_ = dataIn.publisher
.max(by: { (currentItem, nextItem) -> Bool in
return currentItem.city < nextItem.city
})
.sink { [unowned self] profile in Shorthand Argument Names
maxValue = profile.city Note: An even shorter way to write this is to use shorthand
} argument names like this:
}
} .max { $0.city < $1.city }

www.bigmountainstudio.com 178 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)
Use width: 214 .foregroundStyle(.secondary) If tryMax(by:) throws an
} error, then this alert will show.
See how on the next page.
Text("Max 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 180 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: "Igor", city: "Moscow", country: "Russia"),
UserProfile(name: "Rebecca", city: "Atlanta", country: "United States"),
UserProfile(name: "Christina", city: "Stuttgart", country: "Germany"),
UserProfile(name: "Lorenzo", city: "Rome", country: "Italy")]

profiles = dataIn struct InvalidCountryError: Error, Identifiable {


var id = UUID()
_ = dataIn.publisher var country = ""
.tryMax(by: { (current, next) -> Bool in }
if current.country == "United States" {
throw InvalidCountryError(country: "United States")
}
return current.country < next.country
You may notice this code looks a little different from your
})
.sink { [unowned self] (completion) in traditional switch case control flow.
if case .failure(let error) = completion {
self.invalidCountryError = error as? InvalidCountryError This is a shorthand to examine just one case of an enum
}
} receiveValue: { [unowned self] (userProfile) in that has an associate value like failure. This is because
self.maxValue = userProfile.country we re only interested when the completion is a failure.
}
}
You can learn more about if case here.
}

www.bigmountainstudio.com 181 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 { This view shows a collection of data and the
vm.fetch()
minimum values for strings and ints using the
}
}
min operator.
}

www.bigmountainstudio.com 183 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
Pretty simple operator. It will get the
.min()
minimum string or minimum int.
.assign(to: &$minValue)

Finding the minimum value depends on types


let dataInNumbers = [900, 245, 783] conforming to the Comparable protocol.
numbers = dataInNumbers
dataInNumbers.publisher The Comparable protocol allows the Swift
.min() compiler to know how to order objects and
The minimum value is ONLY which is greater or lesser than others.
.assign(to: &$minNumber) published once the publisher has
} sent all of the items through the But what if a type does not conform to the
} pipeline. Comparable protocol? How can you find the
min value?

Then you can use the min(by:) operator.


See next page.

www.bigmountainstudio.com 184 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)
.foregroundStyle(.secondary)
}
In this view, each row is a Profile struct
with a name and city.
Text("Min City: \(vm.minValue)")
And I m getting the minimum city (as a
.bold() string).
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 186 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
Profile(name: "Christina", city: "Stuttgart"),
in the pipeline.
Profile(name: "Lorenzo", city: "Rome"), You can then define your criteria to get the min value.
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
profiles = dataIn
whichever item is last is the minimum.

_ = dataIn.publisher You may have also noticed that the logic is exactly the same
.min(by: { (currentItem, nextItem) -> Bool in as the max(by:) operator. It s because your logic is to
simply define how these items should be ordered and that s
return currentItem.city < nextItem.city
it.
})
.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:
}
.min { $0.city < $1.city }

www.bigmountainstudio.com 187 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)
Use width: 214 .foregroundStyle(.secondary)
}

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 189 Combine Mastery in SwiftUI


Operators

TryMin(by:) - View Model


class TryMin_IntroViewModel: ObservableObject {
struct UserProfile: Identifiable {
@Published var profiles: [UserProfile] = [] let id = UUID()
@Published var maxValue = "" var name = ""
@Published var invalidCountryError: InvalidCountryError? var city = ""
var country = ""
func fetch() { }
let dataIn = [UserProfile(name: "Igor", city: "Moscow", country: "Russia"),
UserProfile(name: "Rebecca", city: "Atlanta", country: "United States"),
UserProfile(name: "Christina", city: "Stuttgart", country: "Germany"),
UserProfile(name: "Lorenzo", city: "Rome", country: "Italy")]

profiles = dataIn

_ = dataIn.publisher
.tryMin(by: { (current, next) -> Bool in
struct InvalidCountryError: Error, Identifiable {
if current.country == "United States" {
var id = UUID()
throw InvalidCountryError(country: "United States") var country = ""
} }
return current.country < next.country
})
.sink { [unowned self] (completion) in
if case .failure(let error) = completion { You may notice this code looks a little different from your
self.invalidCountryError = error as? InvalidCountryError traditional switch case control flow.
}
} receiveValue: { [unowned self] (userProfile) in
This is a shorthand to examine just one case of an enum
self.maxValue = userProfile.country
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 190 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)") This item will be published last after
.sink { [unowned self] datum in
all other items finish.
self.dataToView.append(datum)
}
}
}

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 193 Combine Mastery in SwiftUI


Operators

Append - Multiple
class Append_MultipleViewModel: ObservableObject {
@Published var dataToView: [String] = [] Note: The items are appended
var cancellable: AnyCancellable? AFTER the publisher finishes.
func fetch() { If the publisher never finishes,
let dataIn = ["$100", "$220", "$87", "$3,400", "$12"] the items will never get
appended.
cancellable = dataIn.publisher
.append("Total: $3,819")
.append("(tap refresh to update)") A Sequence publisher is being
.sink { [unowned self] datum in used here which automatically
self.dataToView.append(datum) finishes when the last item is
}
} published. So the append will
} always work here.

struct Append_Multiple: View {


@StateObject private var vm = Append_MultipleViewModel()
Use width: 214
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 194 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 195 Combine Mastery in SwiftUI



Operators

Append - Warning - View Model


class Append_WarningViewModel: ObservableObject {
@Published var dataToView: [String] = []
var cancellable: AnyCancellable?

init() {
Why didn t the items get appended?
cancellable = $dataToView It s because the pipeline never finished. You can see in the Xcode debug console
.append(["Total: $3,819"]) window that the completion never printed.
.append(["(tap refresh to update)"])
.sink { (completion) in Just keep this in mind when using this operator. You want to use it on a pipeline that
print(completion) actually finishes.

} receiveValue: { (data) in
print(data)
}
}

func fetch() {

}
dataToView = ["$100", "$220", "$87", "$3,400", "$12"]
?
}

www.bigmountainstudio.com 196 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 197 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")
Here are two sources of data.
Each pipeline has its own
let read = ["Donny Wals Newsletter", "Dave Verwer Newsletter", "Paul Hudson Newsletter"] property.

.publisher

.prepend("READ")

emails = unread

.append(read) This is where the read pipeline


.sink { [unowned self] datum in is being appended on the
unread pipeline.
self.dataToView.append(datum)

www.bigmountainstudio.com 198 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") { The idea here is that you have a


vm.startPipeline.send(true) publisher that may or may not be
} sending out data. But it won t reach
the subscriber (or ultimately, the UI)
List(vm.data, id: \.self) { datum in unless a second publisher sends out
Text(datum) data too.
}
The second publisher is what opens
Spacer(minLength: 0) the flow of data on the first
publisher.
Button("Close Pipeline") {
This Button sends a value through
vm.cancellables.removeAll()
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 200 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()
When the startPipeline receives a value it sends it straight through and the
init() { Timer publisher detects it and that s when the pipeline is fully connected and data
timeFormatter.timeStyle = .medium 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) Notes
}
• More values sent through the startPipeline have no effect on the Timer s
pipeline.
.store(in: &cancellables)
• 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()

www.bigmountainstudio.com 201 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 { We want the border color around


switch vm.isUserIdValid { the text field to default to gray
case .ok:
(secondary).
return Color.green
case .invalid:
return Color.red If the text is less than 8 characters,
default: we want to change the border color
return Color.secondary 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 203 Combine Mastery in SwiftUI


􀎷
Operators

DropFirst - View Model


enum Validation {
case ok
case invalid
case notEvaluated
}
When the view loads and its view
model is initialized, the pipeline will
class DropFirst_IntroViewModel: ObservableObject { actually run because an empty string is
@Published var userId = "" assigned to userId.
@Published var isUserIdValid = Validation.notEvaluated
This will change the status to invalid
init() { and cause the border to be red before
$userId the user has even done anything.
.dropFirst()
.map { userId -> Validation in The dropFirst will prevent this from Use width: 214
happening and the isUserIdValid
userId.count > 8 ? .ok : .invalid
property will not change.
}
.assign(to: &$isUserIdValid)
}
}

www.bigmountainstudio.com 204 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 Pipeline: The idea here is that I


.dropFirst(2) know the first two items in the data
.sink { [unowned self] datum in
self.dataToView.append(datum) I retrieved are always informational.
}
} So I want to skip them using the
}
dropFirst operator.
struct DropFirst_Count: View {
@StateObject private var vm = DropFirst_CountViewModel()
Use width: 214
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 205 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 207 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 The prefix operator only republishes items up to the number you specify. It will then
.prefix(Int(itemCount)) 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 208 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
.font(.title)
publisher that triggers the stopping of the main pipeline.
.padding(.bottom)
}
}

www.bigmountainstudio.com 210 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
timeFormatter.timeStyle = .medium what turns on the flow of data. To learn more about this
operator, go here.
cancellable = Timer
.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
.drop(untilOutputFrom: startPipeline) Once the prefix operator receives output from the
.prefix(untilOutputFrom: stopPipeline) stopPipeline it will no long republish items coming through
.map { datum in the pipeline. This essentially shuts off the flow of data.
return self.timeFormatter.string(from: datum)
}
.sink{ [unowned self] (datum) in
data.append(datum)
}
}
}

www.bigmountainstudio.com 211 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] = [] No matter how many items
var cancellable: AnyCancellable? come through the pipeline, the
prepend operator will just run
func fetch() {
let dataIn = ["Karin", "Donny", "Shai", "Daniel", "Mark"] one time to send its item
through the pipeline first.
cancellable = dataIn.publisher
.prepend("COMBINE AUTHORS")
.sink { [unowned self] datum in
self.dataToView.append(datum)
}
}
}

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 213 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 -")
This might be a little confusing
.prepend("2022") because the prepend operators at
.sink { [unowned self] datum in the bottom actually publish first.
self.dataToView.append(datum)
}
}
}

struct Prepend_Multiple: View {


Use width: 214 @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 214 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 215 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")
Here are two sources of data.
Each pipeline has its own
let read = ["Donny Wals Newsletter", "Dave Verwer Newsletter", "Paul Hudson Newsletter"] property.

.publisher

.prepend("READ")

emails = read

.prepend(unread) This is where the unread


.sink { [unowned self] datum in pipeline is being prepended on
the read pipeline.
self.dataToView.append(datum)

www.bigmountainstudio.com 216 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 217 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 { Pipeline: The idea here is that
@Published var name = "" we want to “slow down” the input
@Published var nameEntered = ""
so we publish whatever came
init() { into the pipeline every 0.5
$name seconds.
.debounce(for: 0.5, scheduler: RunLoop.main)
.assign(to: &$nameEntered)
}
} The scheduler is basically a
mechanism to specify where and
struct Debounce_Intro: View {
how work is done. I m specifying
@StateObject private var vm = DebounceViewModel()
I want work done on the main
var body: some View { thread. You could also use
VStack(spacing: 20) { DispatchQueue.main.
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 220 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 Mark Mo

www.bigmountainstudio.com 221 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 {
A ProgressView will be shown while the data is being
ProgressView()
} else { fetched. This is done in the view model shown on the
Text(vm.data) next page.
}

Spacer()
}
.font(.title)
}
}

www.bigmountainstudio.com 223 Combine Mastery in SwiftUI


􀎷
Operators

Delay(for: ) - View Model


class DelayFor_IntroViewModel: ObservableObject {
@Published var data = ""
var delaySeconds = 1
@Published var isFetching = false The scheduler is basically a mechanism to
This will show the
specify where and how work is done. I m
ProgressView on
var cancellable: AnyCancellable? specifying I want work done on the main
the view.
thread. You could also use:
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
} receiveValue: { [unowned self] firstValue in many different ways such as:
data = firstValue
} .seconds
} .milliseconds
} .microseconds
This will hide the ProgressView
on the view. .nanoseconds

www.bigmountainstudio.com 224 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
vm.timeEvent.send() The timeEvent property here is a
} PassthroughSubject publisher. You can call send
} with no value to send something down the pipeline
Button(action: {
vm.timeEvent.send() just so we can measure the interval between.
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 226 Combine Mastery in SwiftUI


􀎷
Operators

MeasureInterval - View Model


class MeasureInterval_IntroViewModel: ObservableObject {
The using parameter is a scheduler.
@Published var speed: TimeInterval = 0.0
Which is basically a mechanism to
var timeEvent = PassthroughSubject<Void, Never>()
specify where and how work is done. I m
specifying I want work done on the main
private var cancellable: AnyCancellable? thread. You could also use:
DispatchQueue.main
init() { OperationQueue.main

cancellable = timeEvent
.measureInterval(using: RunLoop.main)
.sink { [unowned self] (stride) in The measureInterval will republish a
Stride type which is basically a form of
speed = stride.timeInterval
elapsed time.
}
} Use width: 214
The timeInterval property will give
} you the value of this time interval
Note, you could also use stride.magnitude : measured in seconds (and fractions of a
second as you can see in the
screenshot).

See if you can beat my


reaction time!

www.bigmountainstudio.com 227 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 { This button will toggle from


Button(startStop ? "Start" : "Stop") {
startStop.toggle() Start to Stop. We re calling the
vm.start() same start function on the view
} model though so it will handle
.frame(maxWidth: .infinity)
turning the pipeline on or off.
Button("Reset") { vm.reset() }
.frame(maxWidth: .infinity)
}

List(vm.data, id: \.self) { datum in


Text(datum)
}
}
.font(.title)
}
}

www.bigmountainstudio.com 229 Combine Mastery in SwiftUI


􀎷

Operators

Throttle - View Model


class Throttle_IntroViewModel: ObservableObject {
@Published var data: [String] = []
var throttleValue: Double = 0.5
For this example, I m using a Timer
private var cancellable: AnyCancellable? publisher to emit values every 0.1
let timeFormatter = DateFormatter()
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) {
cancellable = nil during the interval if false.
} else {
cancellable = Timer
.publish(every: 0.1, on: .main, in: .common) Use width: 214
.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: work done on the main
func reset() { thread. You could also use:
data.removeAll() .seconds
.milliseconds
} DispatchQueue.main
.microseconds
} OperationQueue.main
.nanoseconds

www.bigmountainstudio.com 230 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 {
Use width: 214 ProgressView("Fetching...")
}

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 232 Combine Mastery in SwiftUI


Operators

Timeout - View Model


class Timeout_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = [] This URL isn t real. I wanted
@Published var isFetching = false something that would delay fetching.
@Published var timeoutError: TimeoutError?
private var cancellable: AnyCancellable?

Learn more about the


func fetch() {
dataTaskPublisher here.
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())
Use width: 214
I set the timeout to be super short
.sink(receiveCompletion: { [unowned self] completion in (0.1 seconds) just to trigger it.
isFetching = false

if case .failure(URLError.timedOut) = completion {


timeoutError = TimeoutError()
The scheduler is basically
}
a mechanism to specify
}, receiveValue: { [unowned self] value in
where and how work is
dataToView.append(value)
done. I m specifying I want
})
work done on the main
} struct TimeoutError: Error, Identifiable { thread. You could also use:
let id = UUID()
}
let title = "Timeout"
DispatchQueue.main
let message = "Please try again later."
} OperationQueue.main

www.bigmountainstudio.com 233 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 value2 value1


l
ni
l

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)
.foregroundStyle(.gray)
}
Use width: 214
Text("After using compactMap:")
List(vm.dataWithoutNils, id: \.self) { item in
Text(item)
.font(.title3)
.foregroundStyle(.gray)
}
.frame(maxHeight: 150)
Looking at the screenshot of before and
}
.font(.title) after compactMap, you can see that the
.onAppear { nils were dropped. But you also see that
vm.fetch() “Invalid” was dropped too.
}
}
Let s look at the pipeline and see what
}
happened on the next page.

www.bigmountainstudio.com 236 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 “Invalid” was dropped because inside our
if item == "Invalid" { compactMap we look for this value in
return nil // Will not get republished particular and return a nil.
}
return item Returning a nil inside a compactMap
} closure means it will get dropped.
.sink { [unowned self] (item) in
dataWithoutNils.append(item)
}
}
}
Are nils passed into compactMap? Shorthand Argument Names
Actually, yes. Nils will come in and can be returned If you don t have any logic then you can use
from the closure but they do not continue down the shorthand argument names like this:
pipeline.
.compactMap { $0 }

www.bigmountainstudio.com 237 Combine Mastery in SwiftUI



TryCompactMap
error

nil value5 nil value4 ni value3 value2 value1


l
ni
l

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 239 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


class TryCompactMap_IntroViewModel: ObservableObject {
so the @Published property can be
@Published var dataToView: [String] = []
@Published var invalidValueError: InvalidValueError? observed by the alert modifier on
the previous page.
func fetch() {
let dataIn = ["Value 1", nil, "Value 3", nil, "Value 5", "Invalid"]

_ = dataIn.publisher
.tryCompactMap{ item in
if item == "Invalid" { In this scenario, we throw an error instead
throw InvalidValueError() of dropping the item by returning a nil.
} (See previous example at compactMap.)

return item
}
.sink { [unowned self] (completion) in Since the tryCompactMap indicates a failure can occur
if case .failure(let error) = completion { in the pipeline, you are forced to use the
self.invalidValueError = error as? InvalidValueError sink(receiveCompletion:receiveValue:)
} subscriber.
} receiveValue: { [unowned self] (item) in
dataToView.append(item) Xcode will complain if you just try to use the
} sink(receiveValue:) subscriber.
}
}

www.bigmountainstudio.com 240 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
.font(.title) pipeline.
✅ ❌
} ❌ ✅
}
❌ ✅ ❌
✅ ✅ ✅ ✅
== ? ✅

www.bigmountainstudio.com 242 Combine Mastery in SwiftUI


􀎷
Operators

Filter - View Model


class Filter_IntroductionViewModel: ObservableObject {
@Published var filteredData: [String] = []
In this scenario, we pretend we already
let dataIn = ["Person 1", "Person 2", "Animal 1", "Person 3", "Animal 2", "Animal 3"]
have some fetched data we re working
private var cancellable: AnyCancellable? with (dataIn).

init() { (Most likely your fetch function will populate


filterData(criteria: " ") the filteredData property. This is in here just
to get it initially populated.)
}

func filterData(criteria: String) {


filteredData = []

cancellable = dataIn.publisher Every item that comes through the


pipeline will be checked against your
.filter { item -> Bool in
criteria.
item.contains(criteria)
} If true, the filter operator republishes the
.sink { [unowned self] datum in 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 243 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 This example works like the


Text(datum) previous example except now an
} alert will be displayed if the
} filterError published property on the
.font(.title)
view model becomes not nil.

.alert(item: $vm.filterError) { error in


Alert(title: Text("Error"), message: Text(error.description))
}
}
}

www.bigmountainstudio.com 245 Combine Mastery in SwiftUI


Operators

TryFilter - View Model


struct FilterError: Error, Identifiable {
let id = UUID() The error conforms to Identifiable so
let description = "There was a problem filtering. Please try again." the @Published property can be observed
} by the alert modifier on the previous
class TryFilter_IntroViewModel: ObservableObject {
page.
@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 = []
In this scenario, we throw an error. The sink subscriber will catch it and
cancellable = dataIn.publisher assign it to a @Published property. Once that happens the view will
.tryFilter { item -> Bool in
show an alert with the error message.
if item == "🧨 " {
throw FilterError()
}

return item.contains(criteria)
} Since the tryFilter indicates a failure can occur in the
.sink { [unowned self] (completion) in pipeline, you are forced to use the
if case .failure(let error) = completion {
self.filterError = error as? FilterError sink(receiveCompletion:receiveValue:)
} subscriber.
} receiveValue: { [unowned self] (item) in
filteredData.append(item)
Xcode will complain if you just try to use the
}
} sink(receiveValue:) subscriber.
}

www.bigmountainstudio.com 246 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() If an item coming through the
.sink{ [unowned self] datum in pipeline was the same as the
self.data.append(datum)
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 248 Combine Mastery in SwiftUI


RemoveDuplicates(by:)

1 == 1

12 12 12 12 == 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
.font(.title) removeDuplicates(by:) so it can
.onAppear { determine which objects are equal or not.

vm.fetch()

www.bigmountainstudio.com 250 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: "[email protected]", name: "Joe M."),
UserId(email: "[email protected]", name: "Joseph M."),
UserId(email: "[email protected]", name: "Christina B."),
UserId(email: "[email protected]", name: "Lorenzo D."),
UserId(email: "[email protected]", name: "Enzo D.")]

_ = dataIn.publisher
.removeDuplicates(by: { (previousUserId, currentUserId) -> Bool in If the email addresses are the same, we are
previousUserId.email == currentUserId.email 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 251 Combine Mastery in SwiftUI


TryRemoveDuplicates
error

1 == 1

12 12 12 12 == 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 This example works like the previous


Use width: 214 Text(item.email) example except now an alert will be
} displayed if the removeDuplicateError
published property on the view model
}
becomes not nil.
.font(.title)
.alert(item: $vm.removeDuplicateError) { error in
Alert(title: Text("Error"), message: Text(error.description))
}
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 253 Combine Mastery in SwiftUI


Operators

TryRemoveDuplicates - View Model


struct RemoveDuplicateError: Error, Identifiable {
let id = UUID()
The error conforms to Identifiable so
let description = "There was a problem removing duplicate items." the @Published property can be observed
} by the alert modifier on the previous
page.
class TryRemoveDuplicatesViewModel: ObservableObject {
@Published var dataToView: [UserId] = []
@Published var removeDuplicateError: RemoveDuplicateError?

func fetch() {
let dataIn = [UserId(email: "[email protected]", name: "Joe M."),
UserId(email: "[email protected]", name: "Joseph M."),
UserId(email: "[email protected]", name: "Christina B."),
UserId(email: "N/A", name: "N/A"),
UserId(email: "N/A", name: "N/A")]

_ = dataIn.publisher In this scenario, we throw an error. The sink


.tryRemoveDuplicates(by: { (previousUserId, currentUserId) -> Bool in
subscriber will catch it and assign it to a
if (previousUserId.email == "N/A" && currentUserId.email == "N/A") {
throw RemoveDuplicateError() @Published property. Once that happens the
} view will show an alert with the error message.
return previousUserId.email == currentUserId.email
})
.sink { [unowned self] (completion) in
if case .failure(let error) = completion { Since the tryRemoveDuplicates indicates a failure can
self.removeDuplicateError = error as? RemoveDuplicateError
occur in the pipeline, you are forced to use the
}
} receiveValue: { [unowned self] (item) in sink(receiveCompletion:receiveValue:)
dataToView.append(item) subscriber.
}
} Xcode will complain if you just try to use the
}
sink(receiveValue:) subscriber.

www.bigmountainstudio.com 254 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
.padding() text is changed here.

List(vm.dataToView, id: \.self) { item in


Text(item)
.foregroundStyle(item == vm.noResults ? .gray : .primary)
}
}
.font(.title)
}
}

www.bigmountainstudio.com 256 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) }
If the pipeline finishes and nothing came through it (no matches found), then the
.replaceEmpty(with: noResults) value defined in the replaceEmpty operator will be published.
.sink { [unowned self] (item) in
Note: This will only work on a pipeline that actually finishes. In this scenario, a
dataToView.append(item) Sequence publisher is being used and it will finish by itself when all items have
run through the pipeline.
}

www.bigmountainstudio.com 257 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 260 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
return "*⃣ " + item.uppercased() republish an item. Something always needs to be returned
}) to continue down the pipeline.
.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 Return Removed Using Shorthand Argument Names


.map { item in .map { item in .map { "*⃣ " + $0.uppercased() }
return "*⃣ " + item.uppercased() "*⃣ " + item.uppercased()
} }

Also called “anonymous closure arguments”, use


You can remove the parentheses and the The return keyword is now optional if $0 to refer to the first parameter (item) passed
code will still compile just fine. there is only one line in the closure. into the closure. More info here.

www.bigmountainstudio.com 261 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 262 Combine Mastery in SwiftUI


Operators

Map: Key Path - View Model


struct Creator: Identifiable {
let id = UUID()
This is the object sent down the pipeline.
var fullname = ""
?
}

class Map_KeypathViewModel: ObservableObject {


@Published var dataToView: [String] = []

func fetch() {
let dataIn = [
Creator(fullname: "Mark Moeykens"),
What is a key path?
Creator(fullname: "Karin Prater"),
Creator(fullname: "Chris Ching"), A “key path” is a way to get to a property
Creator(fullname: "Donny Wals"), in an object (struct, class, etc.).
Creator(fullname: "Paul Hudson"),
Maybe it would make more sense if we
Creator(fullname: "Joe Heck")]
called it a “property path”.
You simply provide a key path to the property
_ = dataIn.publisher that you want to send downstream. It does not return a value from a property,
.map(\.fullname) rather it provides directions on how to find
.sink { [unowned self] (name) in Note: You can also used a shorthand it.
argument name too: .map { $0.fullname }
dataToView.append(name)
The map operator will use these
}
directions to find the property, get the
}
value, and then send that value
} downstream.

www.bigmountainstudio.com 263 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 265 Combine Mastery in SwiftUI


Operators

TryMap - View Model


struct ServerError: Error, Identifiable, CustomStringConvertible {
let id = UUID() This will be the error type thrown in the tryMap.
let description = "There was a server error while retrieving values."
} Identifiable
The error conforms to Identifiable so the view s alert
class TryMap_IntroViewModel: ObservableObject {
modifier can observe it and display an Alert.
@Published var dataToView: [String] = []
@Published var error: ServerError?
CustomStringConvertible
func fetch() { This allows us to set a description for our error object
let dataIn = ["Value 1", "Value 2", "Server Error 500", "Value 3"] that we can then use on the UI. You could just as easily
add your own String property to hold an error message.
_ = dataIn.publisher
.tryMap { item -> String in
if item.lowercased().contains("error") {
throw ServerError() Sink
} There are two sink subscribers:
1. sink(receiveValue:)
return item 2. sink(receiveCompletion:receiveValue:)
}
.sink { [unowned self] completion in When it comes to this pipeline, we are forced to use the second one
if case .failure(let error) = completion { because this pipeline can fail. Meaning the publisher and other
self.error = error as? ServerError operators can throw an error.
}
} receiveValue: { [unowned self] item in Xcode s autocomplete won t even show you the first option for this
dataToView.append(item)
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 266 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") You couldn t ask for an easier operator. 😃
.sink { [unowned self] datum in
self.data.append(datum)
}
}
}

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 268 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


The eraseToAnyPublisher operator allows AnyPublisher and
you to simplify the type of your publishers. eraseToAnyPublisher,
look at the chapter
So what are the differences you see between To see an example of how publisher types “Organizing”.
these two pipelines? “nest” and get complex, see this page.
They are pretty similar except the first one throws
Also see:
an error. So the pipeline s failure type is set to
Error and the second one is set to Never. map
tryMap

www.bigmountainstudio.com 270 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
.map { item -> String in This publisher s type is: AnyPublisher<String, Never>
if item == "🧨 " { Even though it will never return an error, this is where you use
setFailureType on the pipeline so it can match the return
return "New Hampshire"
} type of this function.
return item
} Now both publishers match because setFailureType
.setFailureType(to: Error.self) changed the type to: AnyPublisher<String, Error>
.eraseToAnyPublisher()
}
}

www.bigmountainstudio.com 271 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()
Both buttons will call the same function. Two
different publishers are used to get the states.
List(vm.states, id: \.self) { state in
Text(state) 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 272 Combine Mastery in SwiftUI


􀎷
Operators

SetFailureType - View Model


class SetFailureType_IntroViewModel: ObservableObject {
@Published var states: [String] = [] The error needs to conform to Identifiable because it is
@Published var error: ErrorForAlert? needed to work with the SwiftUI alert modifier:

struct ErrorForAlert: Error, Identifiable {


func getPipeline(westernStates: Bool) -> AnyPublisher<String, Error> { let id = UUID()
if westernStates { let title = "Error"
return var message = "Please try again later."
}
["Utah", "Nevada", "Colorado", "🧨 ", "Idaho"].publisher
.tryMap { item -> String in
if item == "🧨 " {
throw ErrorForAlert()
}
return item
}
.eraseToAnyPublisher()
} else {
return
["Vermont", "New Hampshire", "Maine", "🧨 ", "Rhode Island"].publisher
You have a choice here. You can either make both
.map { item -> String in
publishers error-throwing or make both non-error-
if item == "🧨 " {
throwing.
return "Massachusetts"
}
The setFailureType is used to make this pipeline
return item
error-throwing 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 273 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) Because the type returned specifies the possible failure of Error instead of
.sink { [unowned self] (completion) in
Never, it is an error-throwing pipeline.
if case .failure(let error) = completion {
self.error = error as? ErrorForAlert
Xcode will force you to use sink(receiveCompletion:receiveValue:) for
}
} receiveValue: { [unowned self] (state) in error-throwing pipelines.
states.append(state)
} (Non-error-throwing pipelines can use either sink(receiveValue:) or
} assign(to:). )
}

To learn more about the error-


throwing pipelines and how to
convert them to non-error-throwing
pipelines, see the chapter on
“Handling Errors”.

www.bigmountainstudio.com 274 Combine Mastery in SwiftUI


Scan

last value: 123

7 6 5 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)

}
In this example, I am connecting the current item coming through
.font(.title)
the pipeline with the previous item. Then I publish that as a new
.onAppear { item.
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 276 Combine Mastery in SwiftUI




Operators

Scan - View Model


class Scan_IntroViewModel: ObservableObject {

@Published var dataToView: [String] = []

The first time an item comes


func fetch() { through the scan closure
there will be no previous item.
let dataIn = ["1⃣ ", "2⃣ ", "3⃣ ", "4⃣ ", "5⃣ ", "6⃣ ", "7⃣ "]
So you can provide an initial
value to use.

_ = dataIn.publisher

.scan("0⃣ ") { (previousReturnedValue, currentValue) in


What you return from scan
becomes available to look at the
previousReturnedValue + " " + currentValue
next time the current item Use width: 214
} comes through this closure.

.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 277 Combine Mastery in SwiftUI


TryScan
error

last value: 123


!
7 5 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 279 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 {
throw InvalidValueFoundError() let message = "Invalid value was found: "
}
}
return previousReturnedValue + " " + currentValue
}
.sink { [unowned self] (completion) in
if case .failure(let error) = completion {
if let err = error as? InvalidValueFoundError {
The error message is just being appended to our data
dataToView.append(err.message + invalidValue)
to be displayed on the view.
}
}
} receiveValue: { [unowned self] (item) in
dataToView.append(item)
}
}
}

www.bigmountainstudio.com 280 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)
.onAppear {
In this example, we run through 25 numbers and
arrange them in a lazy grid.
vm.fetch()
}
If the Circles toggle is changed then the pipeline that
}
composes all of the image names is run again.
}

www.bigmountainstudio.com 283 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.
func fetch() { This means you don t need to store a cancellable.
cachedData = Array(1...25)
If you were to do this without using collect, it
formatData(shape: circles ? "circle" : "square")
would look something like this:
}
func formatData(shape: String) {
func formatData(shape: String) { dataToView.removeAll()

cachedData.publisher
cachedData.publisher
.map { "\($0).\(shape)" } .map { "\($0).\(shape)" }
.collect() .sink { [unowned self] item in
.assign(to: &$dataToView) dataToView.append(item)
}
}
.store(in: &cancellables)
} }

www.bigmountainstudio.com 284 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: ", "))
I m using the collect operator to form
}
teams of two, which is actually an
}
array with two items.
.font(.title)
.onAppear {
When the slider changes value, I m
vm.fetch()
The joined function puts all the items using another pipeline to trigger the
}
in an array into a single string, recreation of this data into teams of 3
}
separated by the string you specify. and 4.
}

www.bigmountainstudio.com 286 Combine Mastery in SwiftUI


􀎷


Operators

Collect By Count - View Model


class Collect_ByCountViewModel: ObservableObject {
@Published var teamSize = 2.0
@Published var teams: [[String]] = [] A reference to the teamSize pipeline is stored
private var players: [String] = [] in cancellables. So why isn t the players
private var cancellables: Set<AnyCancellable> = []
pipeline in the createTeams function stored
too?
init() {
$teamSize
You need to keep the teamSize pipeline alive
.sink { [unowned self] in createTeams(with: Int($0)) }
.store(in: &cancellables) because it s actively connected to a slider on
} the view.

func fetch() { But you don t need to store a reference to the


players = ["Mattie", "Chelsea", "Morgan", "Chase", "Kristin", "Beth", "Alex", "Ivan", players pipeline because you use it one time
"Hugo", "Rod", "Lila", "Chris"] and then you are done.

createTeams(with: Int(teamSize))
}

func createTeams(with size: Int) {


teams.removeAll()

_ = players.publisher
.collect(size) All of the player names will go through this pipeline
.sink { [unowned self] (team) in and be group together (or collected) into arrays using
teams.append(team) the collect operator.
}
}
}

www.bigmountainstudio.com 287 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.
.font(.title) Every time something is published, I send a 🟢 down the
} pipeline instead. These are collected into an array every 0.7
} seconds and then published.

www.bigmountainstudio.com 289 Combine Mastery in SwiftUI


Operators

Collect By Time - View Model


class Collect_ByTimeViewModel: ObservableObject {
Since the collect operator
@Published var timeInterval = 0.5
publishes arrays, I created an
@Published var collections: [[String]] = []
array of arrays type to hold
private var cancellables: Set<AnyCancellable> = []
everything published.
private var timerCancellable: AnyCancellable?

init() {
$timeInterval
Every time timeInterval changes
.sink { [unowned self] _ in fetch() }
(slider moves), call fetch().
.store(in: &cancellables)
}

func fetch() { Since the fetch function will get called


collections.removeAll() repeatedly as the slider is moving, I m canceling
Use width: 214
timerCancellable?.cancel() the pipeline so it starts all over again.

timerCancellable = Timer
.publish(every: 0.1, on: .main, in: .common) Replace anything that comes down
.autoconnect()
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 290 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")
From what I can see from
Use width: 214 Text("Collections") experimentation, it seems to
.bold() publish when both the count
and interval are reached.
List(vm.collections, id: \.self) { items in
When you look at the
Text(items.joined(separator: " "))
screenshot, it is publishing
}
every 4 items AND, after one
}
second, it publishes whatever is
.font(.title) remaining.
.onAppear {
vm.fetch() I could be wrong on this but I
} The joined function puts all the items couldn t find any good
in an array into a single string, documentation that breaks this
}
separated by the string you specify. down clearly.
}

www.bigmountainstudio.com 292 Combine Mastery in SwiftUI



Operators

Collect by Time or Count - View Model


class Collect_ByTimeOrCountViewModel: ObservableObject {

@Published var collections: [[String]] = []

private var timerCancellable: AnyCancellable? RunLoop.main is basically


a mechanism to specify
where and how work is
func fetch() { done. I m specifying I want
collections.removeAll() work done on the main
thread. You could also use: The delay can be specified in
timerCancellable?.cancel()
many different ways such as:
DispatchQueue.main
OperationQueue.main
timerCancellable = Timer .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 293 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)

List(vm.dataToView, id: \.self) { item in


Text(item)
}
Use width: 214 These two List views are
Text("Ignore Output:") actually using the same
.bold() publisher.

List(vm.dataToView2, id: \.self) { item in


The only difference is the
Text(item)
second pipeline is using
}
}
the ignoreOutput
.font(.title) operator.
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 295 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
_ = dataIn.publisher values never made it
.ignoreOutput() through the pipeline
because they were
.sink(receiveCompletion: { [unowned self] completion in
ignored.
dataToView2.append("Pipeline Finished")

}, receiveValue: { [unowned self] _ in You also can see the


receiveValue closure
dataToView2.append("You should not see this.")
was never run either but
}) the receiveCompletion
} was.
}

www.bigmountainstudio.com 296 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 298 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


_ = dataIn.publisher
operator signature might look a little familiar.
.sink { [unowned self] (item) in
animals.append(item) The first parameter is a default value so the first item
} has something it can be compared to or examined in
some way.

dataIn.publisher
The closure s input parameter named
.reduce("") { (longestNameSoFar, nextName) in longestNameSoFar is actually the previous item that
if nextName.count > longestNameSoFar.count { was returned from the reduce operator.
return nextName
The nextName is the current item.

}
return longestNameSoFar
}
Shorthand Argument Names
.assign(to: &$longestAnimalName)
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 299 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
.font(.title) not nil it will present an alert.
.onAppear {
vm.fetch()
}
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}

www.bigmountainstudio.com 301 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
_ = dataIn.publisher error is conforming to Identifiable so it can be monitored with an
.tryReduce("") { (longestNameSoFar, nextName) in
alert modifier on the view:
if nextName.contains("tree") {
throw NotAnAnimalError()
} struct NotAnAnimalError: Error, Identifiable {
let id = UUID()
if nextName.count > longestNameSoFar.count { let message = "We found an item that was not an animal."
}
return nextName
}
return longestNameSoFar
}
.sink { [unowned self] completion in
if case .failure(let error) = completion { When using a try operator the pipeline recognizes that it can now fail.
self.error = error as? NotAnAnimalError
So a sink with just receiveValue will not work. The error should be
}
} receiveValue: { [unowned self] longestName in handled in some way so the sink s completion will assign it to a
longestAnimalName = longestName published property to be shown on the view.
}
}
}

www.bigmountainstudio.com 302 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 305 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 The first operator will just return one item. Since the
pipeline will finish right after that, we can use the
.first()
assign(to:) subscriber and set the published
.assign(to: &$firstGuest)
property.
}
}

www.bigmountainstudio.com 306 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)
The criteria property
TextField("search criteria", text: $vm.criteria) changing is what triggers
.textFieldStyle(RoundedBorderTextFieldStyle()) the search.
.padding()

Text("First Found: ") + Text(vm.firstFound).bold()

Form {
List(vm.deviceList, id: \.self) { device in
Text(device)
}
}
}
.font(.title) The idea here is to use the first(where:)
.onAppear { operator to find the first device that matches
vm.fetch() the user s search criteria.
}
}
}

www.bigmountainstudio.com 308 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?


The dollar sign ($) is used to access the criteria s
publisher. Every time the criteria changes, its value
init() {
is sent through the pipeline.
criteriaCancellable = $criteria
.sink { [unowned self] searchCriteria in
Note: You could probably improve this pipeline
findFirst(criteria: searchCriteria)
with some additional operators such as debounce
}
and removeDuplicates.
}

func fetch() {
deviceList = ["iPhone 4", "iPhone 15", "iPad Pro (14-inch)", "MacBook Pro 20-inch"]
}
When the first device is found to match the criteria, it ll be assigned to the
func findFirst(criteria: String) { firstFound and the pipeline will finish.
deviceList.publisher If nothing is found then the replaceEmpty operator will return “Nothing found”.
.first { device in
device.contains(criteria)
}
.replaceEmpty(with: "Nothing found") Shorthand Argument Names
.assign(to: &$firstFound) Note: An even shorter way to write this is to use
} shorthand argument names like this:
}
.first { $0.contains(criteria) }

www.bigmountainstudio.com 309 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
}
.font(.title) error property, this alert modifier will
.onAppear { detect it and present an Alert.
vm.fetch()
}
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message))
}
}
}

www.bigmountainstudio.com 311 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)
} In this example, we are going to throw an error and assign it to the error
.store(in: &cancellables) published property so the view can get notified. The error conforms to
} Identifiable so the alert modifier on the view can use it:
func findFirst(criteria: String) {
deviceList.publisher struct InvalidDeviceError: Error, Identifiable {
.tryFirst { device in let id = UUID()
if device.contains("Google") { let message = "Whoah, what is this? We found a non-Apple device!"
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
} Learn More
} receiveValue: { [unowned self] foundDevice in
firstFound = foundDevice • dropFirst
}
.store(in: &cancellables)
• debounce
} • replaceEmpty
}

www.bigmountainstudio.com 312 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:")
The last operator is being used to get the last
Text(vm.destination)
city in the user s list of destinations.
.bold()

Use width: 214 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 314 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 The last operator will just return one item when the
.last() pipeline finishes. Because of that, we can use the
.replaceEmpty(with: "Enter a city") assign(to:) subscriber and set the published
.assign(to: &$destination) 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 315 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:")


The view model has a pipeline that will use
Use width: 214 Text(vm.lastMan) the last operator to filter out all the men that
are on Earth and find the last one.
.bold()
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 317 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"),
Specify criteria in the closure and after the pipeline finishes,
Alien(name: "Ellen", gender: "female", planet: "Venus")]
the last of whatever is remaining will be published.

dataIn.publisher
.last(where: { alien in
alien.gender == "man" && alien.planet == "Earth"
})
.map { $0.name } Shorthand Argument Names
Let s use map to republish
.assign(to: &$lastMan) Note: An even shorter way to write this is to use shorthand
just the name.
} argument names like this:
}
.last { $0.gender == "man" && $0.planet == "Earth" }

www.bigmountainstudio.com 318 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

TryLast(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 {
Use width: 214 Text(alien.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(alien.planet)
.foregroundStyle(.gray)
} If an error is assigned to the view model s
}
error property, this alert modifier will
}
} 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 320 Combine Mastery in SwiftUI



Operators

TryLast(where:) - View Model


class TryLast_WhereViewModel: ObservableObject {
@Published var lastMan = ""
@Published var aliens: [Alien] = []
@Published var error: InvalidPlanetError?

func fetch() {
aliens = [Alien(name: "Rick", 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: "Morty", gender: "man", planet: "Earth"),
Alien(name: "Ellen", gender: "female", planet: "Venus"),
Alien(name: "Flippy", gender: "non-binary", planet: "Pluto")]

_ = aliens.publisher
.tryLast(where: { alien in In this example, we are going to throw an error and assign it to the error
if alien.planet == "Pluto" { published property so the view can get notified. The error conforms to
throw InvalidPlanetError() Identifiable so the alert modifier on the view can use it:
}
struct InvalidPlanetError: Error, Identifiable {
return alien.gender == "man" && alien.planet == "Earth" let id = UUID()
}) let description = "Pluto is not a planet. Get out of here!"
.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
}
}
}

www.bigmountainstudio.com 321 Combine Mastery in SwiftUI


Output(at:)
2
7 6 5 4 3 2

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-of-bounds 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.")

The Stepper is bound to


Stepper("Index: \(vm.index)", value: $vm.index)
the index property which
.padding(.horizontal)
will call a function to get
Text("Animal: \(vm.selection)")
the animal at that index
.italic()
using the output(at:)
.font(.title3)
operator.
.foregroundStyle(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)

Text("Smart Animals")
.bold()
List(vm.animals, id: \.self) { animal in
Text(animal)
}
}
.font(.title)
}
}

www.bigmountainstudio.com 323 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() {

cancellable = $index
When the stepper on the view changes the
index property, we want to call getAnimal
.sink { [unowned self] in getAnimal(at: $0)} using the new property.
}

func getAnimal(at index: Int) {

animals.publisher Once the right item at the index is found, the


.output(at: index) pipeline finishes and sets the value to the
published property.
.assign(to: &$selection)

www.bigmountainstudio.com 324 Combine Mastery in SwiftUI


Output(in:)
2…4
8 7 6 5 4 3 2

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)

Increasing and decreasing


List(vm.animals) { animal in the steppers will narrow down
Text("\(animal.index): \(animal.name)") the items in the list using the
output(in:) operator.
}

.font(.title)

www.bigmountainstudio.com 326 Combine Mastery in SwiftUI


􀎷
Operators

Output(in:) - View Model


class Output_InViewModel: ObservableObject {
@Published var startIndex = 0 The Animal struct conforms to Identifiable so it can be iterated
@Published var endIndex = 5 though on the UI:
@Published var animals: [Animal] = []
struct Animal: Identifiable {
let id = UUID()
var cancellables: Set<AnyCancellable> = [] var index = 0
var name = ""
let cache = [Animal(index: 0, name: "Chimpanzee"), }
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 { Unlike the output(at:) operator which returns one item at an index,
return 0 the output(in:) operator will crash your app if the index goes out
} else if index > endIndex { of bounds. So you will have to make sure the start index does not
return endIndex 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 327 Combine Mastery in SwiftUI


Operators

$endIndex
If the end index becomes less than the start index, the app will crash.
.map { [unowned self] index in
But if the end index becomes greater than the number of items that
index < startIndex ? startIndex : index
come through the pipeline you are safe.
}
(Note: You could also control this on the UI or with other methods.)
.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 328 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


At a store, the employees will greet and talk to customers. If the customer employees in the foreground can keep doing their jobs -
has a computer that needs work done, many times it will get sent to the greeting and talking to customers. When the computer is
background to get looked at and fixed. ready, it gets sent to the foreground.

www.bigmountainstudio.com 330 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 In the background, work that might take longer is performed
to get data from the internet, it could take a while so you send it to the so the UI can keep doing its job and talking to the user. When
background to go get the data. (This is usually called the “main thread”.) the image is fetched, it sends it back to the foreground.

www.bigmountainstudio.com 331 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


Hey, you publishers and
the background. Let s move it to
operators upstream, I want you to do
the foreground (main) thread and
your work in the background.
send it downstream.

subscribe receive

www.bigmountainstudio.com 332 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()

In this example, a URL is used


Spacer(minLength: 0) to retrieve an image on a
} background thread, and then
.font(.title) it is moved to a foreground
(main) thread to be displayed
}
on the UI.
}

www.bigmountainstudio.com 334 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
.receive(on: RunLoop.main) navigator that it s not on the main thread.
.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
}
}, receiveValue: { [unowned self] image in specifying I want work done on the
imageView = image main thread. You could also use
}) these other schedulers:
.store(in: &cancellables) RunLoop
} Run loops manage events and
DispatchQueue.main
} work. It allows multiple things OperationQueue.main
to happen simultaneously.

www.bigmountainstudio.com 335 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 2. Purple warning in Xcode editor

3. Message in Issue navigator 4. Message in debug console

When you see these things, you know it is time to use receive(on:).

www.bigmountainstudio.com 336 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
.font(.title)
assign, says, “Hey, I would like some data now.”
.onAppear {
2. Receive Output - This is when an item is coming through the pipeline
vm.fetch() 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”.

www.bigmountainstudio.com 338 Combine Mastery in SwiftUI


Operators

Subscribe(on:) - View Model


class Subscribe_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
The handleEvents operator
func fetch() { is a great way to demonstrate
let dataIn = ["Which", "thread", "is", "used?"] and show where the 4
operations are doing their
_ = dataIn.publisher
.map { item in work.
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 Learn More
print("receiveCompletion: Main thread? \(Thread.isMainThread)") Learn more about
}, receiveCancel: {
print("receiveCancel: Main thread? \(Thread.isMainThread)") handleEvents in the
}, receiveRequest: { demand in Debugging chapter.
print("receiveRequest: Main thread? \(Thread.isMainThread)")
})
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink { [unowned self] item in
dataToView.append(item)
}
} d
} g roun
b ack
Even though subscribe(on:) is added to the pipeline, the All
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 339 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 342 Combine Mastery in SwiftUI


Assign(to:)

View Model
class AssignToViewModel: ObservableObject {
@Published var name = "" Pipeline: Whenever the name changes, the greeting is automatically
@Published var greeting = ""
updated.
init() {
$name
.map { [unowned self] name in
createGreeting(with: name)
} No AnyCancellable
.assign(to: &$greeting) 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.

func createGreeting(with name: String) -> String { When this view model is de-initialized and then the @Published
let hour = Calendar.current.component(.hour, from: Date())
properties de-initialize, the pipeline will automatically be canceled.
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 343 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 345 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"] Pipeline: The idea here is when a new
value is assigned to newName, it is
@Published var newName = "" examined and decided which array to
@Published var aToM: [String] = []
add it to.
@Published var nToZ: [String] = []
var cancellable: AnyCancellable?

The first value to come through is the Note: There are two types of pipelines:
init() { empty string the newName property is
cancellable = $newName
• Error-throwing
assigned. We want to skip this by using the
• Non-Error-Throwing
.dropFirst() dropFirst operator.
.sink { [unowned self] (name) in
You can ONLY use
let firstLetter = name.prefix(1) sink(receiveValue:) on non-error-
if firstLetter < "M" { throwing pipelines.
aToM.append(name) If the value coming through the pipeline was
} else { always assigned to the same @Published Not sure which kind of pipeline you
nToZ.append(name) property, you could use the assign(to:) have?
} subscriber instead. 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 346 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) The goal here is to show the
ProcessingView while the
if vm.isProcessing { ProcessingView() }
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...")
.foregroundStyle(.white)
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 15).fill(Color.black.opacity(0.9)))
}
}

www.bigmountainstudio.com 347 Combine Mastery in SwiftUI


􀎷

Sink(receiveCompletion: receiveValue:)

Sink Completion - View Model


class SinkCompletionViewModel: ObservableObject {
Pipeline: The idea here is when some operation
@Published var data = ""
has started, show the progress indicator and
@Published var isProcessing = false when the pipeline completes, turn it off.
var cancellables: Set<AnyCancellable> = []

func fetch() {
isProcessing = true This will trigger showing the ProcessingView.

[1,2,3,4,5].publisher
Add some extra time to this pipeline to
.delay(for: 1, scheduler: RunLoop.main)
slow it down.
.sink { [unowned self] (completion) in
isProcessing = false
} receiveValue: { [unowned self] (value) in When completed, this will hide the Use width: 214
data = data.appending(String(value)) ProcessingView.
}
.store(in: &cancellables)
}
}

Learn More
• delay
• See another example of hiding/
showing the ProgressView using the
handleEvents operator

www.bigmountainstudio.com 348 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
Text(vm.data) true then the error will show.
}
.font(.title)
.alert(isPresented: $vm.showErrorAlert) {
Alert(title: Text("Error"), message: Text(vm.errorMessage))
}
}
}

www.bigmountainstudio.com 349 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() { Pipeline: The idea here is to check values


cancellable = [1,2,3,4,5].publisher
coming through the pipeline and stop if
.tryMap { (value) -> String in
if value >= 5 { some condition is met.
throw NumberFiveError()
}
return String(value) Use width: 214
}
.sink { [unowned self] (completion) in
switch completion { In this example, we re examining the
case .failure(_): completion input parameter to see if there
showErrorAlert.toggle() was a failure. If so, then we toggle an
case .finished:
indicator and show an alert on the view.
print(completion)
}
data = String(data.dropLast(2))
} receiveValue: { [unowned self] (value) in
data = data.appending("\(value), ")
}
}
}

www.bigmountainstudio.com 350 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 353 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> { If you re adding operators, you might find it easier to use a closure. If there s only one
Just("Moeykens") item in a closure then you don t need to use the get or the return keywords.
.map { $0.uppercased() }
}

func teamPipeline(uppercased: Bool) -> AnyCancellable {


["Lisandro", "Denise", "Daniel"].publisher You can also have functions that return whole pipelines. The sink
.map { subscribers return AnyCancellable. The assign(to:) does not.
uppercased ? $0.uppercased() : $0
}
.sink { [unowned self] name in
team.append(name)
}
}
“Should I use a property
func fetch() {
firstNamePublisher
From here, you can just attach operators or a function?”
.map { $0.uppercased() } and subscribers to your publisher
.assign(to: &$firstName) properties. My own personal rule is I always
start with a property.
lastNameUppercased
.assign(to: &$lastName)
If you re returning a whole pipeline, then just But then if the pipeline needs to
_ = teamPipeline(uppercased: false) use a variable then I convert it to a
call the function and handle the returned
} cancellable in some way. function and pass in the variable.
}

www.bigmountainstudio.com 354 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<
let publisher = URLSession.shared.dataTaskPublisher(for: url)
Publishers.Concatenate<
.map { (data: Data, response: URLResponse) in Publishers.Sequence<[String], Error>,
data Publishers.ReceiveOn<
} Publishers.Decode<
Publishers.Map<
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: RunLoop.main) URLSession.DataTaskPublisher,
.prepend("AWAY TEAM") JSONDecoder.Input>
.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>>> {
. . .
}

If you OPTION-Click on publisher, you can inspect the type. There s a better way!
Instead, you can just return AnyPublisher. Yes, ONE type.

www.bigmountainstudio.com 356 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

Before After
func publisher(url: URL) -> func publisher(url: URL) -> AnyPublisher<String, Never> {
Publishers.ReplaceError<Publishers.Concatenate<Publishers.S return URLSession.shared.dataTaskPublisher(for: url)
equence<[String], Error>, .map { (data: Data, response: URLResponse) in
Publishers.ReceiveOn<Publishers.Decode<Publishers.Map<URLSe data
ssion.DataTaskPublisher, JSONDecoder.Input>, String, }
JSONDecoder>, RunLoop>>> { .decode(type: String.self, decoder: JSONDecoder())
return URLSession.shared.dataTaskPublisher(for: url) .receive(on: RunLoop.main)
.map { (data: Data, response: URLResponse) in .prepend("AWAY TEAM")
data .replaceError(with: "No players found")
} .eraseToAnyPublisher()
.decode(type: String.self, decoder: JSONDecoder()) }
.receive(on: RunLoop.main)
.prepend("AWAY TEAM")
.replaceError(with: "No players found")
} 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
This is a great solution for simplifying return types when using a
simple type like String and then read the error message. It will tell you.
function.

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 357 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)
}
}
.font(.title) 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.

www.bigmountainstudio.com 358 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)
There is a pipeline on this toggle so
}
when the value changes, it re-fetches
.store(in: &cancellables)
the data to populate the list.
}
Use width: 214
func fetch(homeTeam: Bool) {
team.removeAll()
AppPublishers.teamPublisher
AppPublishers.teamPublisher(homeTeam: homeTeam) returns a publisher that either gets the
.sink { [unowned self] item in home team or the away team.
team.append(item)
These are two different pipelines that
}
can be returned from the same function
.store(in: &cancellables)
but use the same subscriber.
}
} Let s see how this is done on the next
page.

www.bigmountainstudio.com 359 Combine Mastery in SwiftUI



Organizing

AppPublishers.teamPublisher
class AppPublishers {

static func teamPublisher(homeTeam: Bool) -> AnyPublisher<String, Never> { There may be a scenario in your app where you
if homeTeam { 1 need the same publisher on multiple views.
Instead of duplicating the publisher, you can
return ["Stockton", "Malone", "Williams"].publisher
extract it to a common class like this.
.prepend("HOME TEAM")

.eraseToAnyPublisher()

} else { I m using hard-code values here for demonstration


purposes. But let s suppose that these values are
let url = URL(string: "https://www.nba.com/api/getteam?id=21")!
2 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
data and never fail (meaning they don t throw
} errors).
2. This is a fake URL to get a team based on an id.
.decode(type: String.self, decoder: JSONDecoder())
3. If you have read about dataTaskPublisher
.receive(on: RunLoop.main) then you know errors can be thrown. So to
.prepend("AWAY TEAM") make both pipelines return the same type of
3 AnyPublisher that never returns errors I use
.replaceError(with: "No players found")
the replaceError operator to intercept
.eraseToAnyPublisher() errors, return a String and cancel the
} publisher.
}
Read more about this in the “Handling Errors”
} chapter.

www.bigmountainstudio.com 360 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)
Use width: 214 Text(vm.artData.artist)
.font(.body)
}
.padding()
.background(vm.artData.color.opacity(0.3))
.padding()

}
.font(.title)
.onAppear { There are two publishers with many artists and many colors. But
vm.fetch() 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 363 Combine Mastery in SwiftUI


Working with Multiple Publishers

CombineLatest - View Model


class CombineLatest_IntroViewModel: ObservableObject {

@Published var artData = ArtData()


The combineLatest receives the latest values from both
pipelines in the form of a Tuple.
func fetch() { The data is used to instantiate a new ArtData object and
sent down the pipeline.
let artists = ["Picasso", "Michelangelo", "van Gogh", "da Vinci", "Monet"]

let colors = [Color.red, Color.orange, Color.blue, Color.purple, Color.green] struct ArtData: Identifiable {
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

} By the way, I have photos in


the asset catalog that match
all the artists names.

www.bigmountainstudio.com 364 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)
Use width: 214 Text(vm.artData.artist)
.font(.body)
}
.padding()
.background(vm.artData.color.opacity(0.3))
.padding()

}
.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 365 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() {
The three publishers used all have varying amounts of
let artists = ["Picasso", "Michelangelo"]
data. But remember, the combineLatest is only
let colors = [Color.red, Color.purple, Color.blue, Color.orange]
interested in the latest value the publisher sends down
let numbers = [1, 2, 3] 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 366 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] You can also use the
let numbers = [1, 2, 3] CombineLatest function directly
from the Publishers enum. There
are 3 different options:
_ = Publishers.CombineLatest3(artists.publisher, colors.publisher, numbers.publisher)
.map { (artist, color, number) in CombineLatest for 2 publishers
return ArtData(artist: artist, color: color, number: number) CombineLatest3 for 3 publishers
} CombineLatest4 for 4 publishers
.sink { [unowned self] (artData) in
self.artData = artData
}
}
}
When using Publishers.CombineLatest, you will have to
include a map operator since there is no closure for code.

www.bigmountainstudio.com 367 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
.font(.title)
} each name that comes
down the pipeline.
func getPercent(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .percent
Notice the order of the
return formatter.string(from: NSNumber(value: number)) ?? "N/A" results does not match
} the order of the names
}
above the button.

www.bigmountainstudio.com 369 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"] The main publisher is the list of names. For each
@Published var nameResults: [NameResult] = []
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
.map { name -> (String, URL) in closure.
(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 { (data: Data, response: URLResponse) in .map { $0.data } or .map(\.data).
data
}
.decode(type: NameResult.self, decoder: JSONDecoder())
.replaceError(with: NameResult(name: name, gender: "Undetermined")) If there is an error from either the
.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 370 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
func fetchNameResults() {
pipeline and set the failure type to Never.
names.publisher
.map { name -> (String, URL) in
I didn t have to set the return type of flatMap. It will
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
work just fine without it but I wanted it here so you
}
could see it and it would be more clear.
.flatMap { (name, url) -> AnyPublisher<NameResult, Never> in
URLSession.shared.dataTaskPublisher(for: url)
You could throw an error from flatMap if you wanted
.map { (data: Data, response: URLResponse) in
to. You would just have to change the subscriber from
data
sink(receiveValue:) to
}
sink(receiveCompletion:receiveValue:).
.decode(type: NameResult.self, decoder: JSONDecoder())
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
See more at “Handling Errors”.
.eraseToAnyPublisher()
}
.receive(on: RunLoop.main)
The receive operator
.sink { [unowned self] nameResult in
switches execution back to
nameResults.append(nameResult)
the main thread. If you don t
} do this, Xcode will show you a
.store(in: &cancellables) purple warning and you may
} or may not see results appear
} on the UI.

www.bigmountainstudio.com 371 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
data Different Use width: 214
} order
.decode(type: NameResult.self, decoder: JSONDecoder())
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher()
}
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in
You can t guarantee the order in which the results are
nameResults.append(nameResult)
returned from this flatMap. All of the publishers can run
}
all at the same time.
.store(in: &cancellables)
You CAN control how many publishers can run at the same
}
time though with the maxPublishers parameter.
}
See next page…

www.bigmountainstudio.com 372 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 373 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)
}
} You can merge up to seven additional publishers of
.font(.title) the same type to your main publisher.
.onAppear {
vm.fetch()
}
Use width: 214 }
}
You can see from the
class Merge_IntroViewModel: ObservableObject {
@Published var data: [String] = [] order on the
screenshot how
func fetch() { these sequence
let artists = ["Picasso", "Michelangelo"]
let colors = ["red", "purple", "blue", "orange"] publishers all got
let numbers = ["1", "2", "3"] merged together.

_ = artists.publisher
.merge(with: colors.publisher, numbers.publisher)
Other types of
.sink { [unowned self] item in merged publishers
data.append(item) will just publish their
}
items as they come
}
} in.

www.bigmountainstudio.com 375 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 {
Use width: 214 Text(nameResult.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(nameResult.gender + ": ") This example is very
Text(getPercent(nameResult.probability)) similar to the flatMap
} example except now it
}
} uses map and
.font(.title) switchToLatest.
}
That s why you only
func getPercent(_ number: Double) -> String {
let formatter = NumberFormatter() see the last name,
formatter.numberStyle = .percent “Tracy”, because it was
return formatter.string(from: NSNumber(value: number)) ?? "N/A" the last publisher that
}
} came down the
pipeline.

www.bigmountainstudio.com 377 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
.map { name -> (String, URL) in
in the Organizing chapter.
(name, URL(string: "https://api.genderize.io/?name=\(name)")!)
}
.map { (name, url) in
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data Using the URL created with the name,
} another publisher is created and sent
.decode(type: NameResult.self, decoder: JSONDecoder()) down the pipeline.
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: RunLoop.main) The switchToLatest operator will only
.sink { [unowned self] nameResult in republish the item published by the latest
The receive operator switches execution
nameResults.append(nameResult) dataTaskPublisher that came through.
back to the main thread. If you don t do this,
} OK, that s a mouthful. Let s look at a
Xcode will show you a purple warning and you
.store(in: &cancellables) diagram on the next page.
may or may not see results appear on the UI.
}
}

www.bigmountainstudio.com 378 Combine Mastery in SwiftUI





Working with Multiple Publishers

SwitchToLatest - Diagram

Tracy You are the latest


publisher. Publish your value and I
will send it down the pipeline.

Taylor

All 6 publishers come in one after another


1 and only the latest one (the last one, in this
case) is used to publish its value.
Alexus
Pipeline

Pat

struct NameResult: Decodable


{
Publish var name = "Tracy"
Tracy var gender = "female"
Madison var probability = 0.92
}

The dataTaskPublisher publishes its


Kelly 2
value and sends it downstream.

www.bigmountainstudio.com 379 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 { In this example, every


Text(vm.nameResult?.name ?? "Select a name") time you tap a row an
.frame(maxWidth: .infinity, alignment: .leading) API is called to get
Text((vm.nameResult?.gender ?? "") + ": ")
Text(getPercent(vm.nameResult?.probability ?? 0)) information.
}
.padding() If you tap many rows
.border(Color.gold), width: 2)
then that could mean a
} lot of network traffic.
.font(.title)
}
Using
func getPercent(_ number: Double) -> String { switchToLatest will
let formatter = NumberFormatter() automatically cancel
formatter.numberStyle = .percent all previous network
return formatter.string(from: NSNumber(value: number)) ?? "N/A"
} calls and only run the
} latest one.

www.bigmountainstudio.com 380 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?
A PassthroughSubject is the publisher this time.
var fetchNameDetail = PassthroughSubject<String, Never>()

Only one name will be sent through at a time. But many


private var cancellables: Set<AnyCancellable> = []
names can come through.
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()
.receive(on: RunLoop.main)
.sink { [unowned self] nameResult in Learn More
self.nameResult = nameResult • dataTaskPublisher
}
If the user is tapping many rows, the switchToLatest • replaceError
.store(in: &cancellables)
}
operator will keep canceling dataTaskPublishers until • delay
} one finishes and then sends the results downstream. • eraseToAnyPublisher

www.bigmountainstudio.com 381 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)
Use width: 214 Text(artData.artist)
.font(.body)
}
.padding(4)
.background(artData.color.opacity(0.4))
.frame(height: 150)
}
}
}
.font(.title)
.onAppear { There are two publishers, one for an artist s name
vm.fetch()
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 383 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
dataToView.append(item) Use width: 214
}
Note: Items only get
} published when there is a
} value from BOTH publishers.

If you were to remove


Color.green from the
The zip operator will match up items from each publisher and colors array then “Monet”
pass them as input parameters into its closure. would not get published. It is ?
because “Monet" would not
In this example, both input parameters are used to create a new have a matching value from
ArtData object and then send that down the pipeline. the colors array anymore.

www.bigmountainstudio.com 384 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. There are pipelines that never throw errors. They have
Operators that begin with “try” are good examples. Xcode will publishers that are incapable of throwing errors and
let you add error handling to these pipelines. downstream there are no “try” operators that throw errors.
Xcode will NOT let you add error handling to these pipelines.

publisher publisher
.try… { … } .map { … }
.sink(receiveCompletion: { … }, .sink(receiveValue: { … })
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 Xcode WILL allow you to use sink(receiveValue:), or
in the example above) to handle the error that caused the failure. sink(receiveCompletion:receiveValue:), or assign(to:).
You also cannot use assign(to:). That subscriber is for non-error The assign(to:) subscriber is for non-error throwing pipelines only.
throwing pipelines only. Xcode will show you an error if you try.

www.bigmountainstudio.com 386 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.

This error handling operator changes this error-throwing pipeline back into a pipeline
Non-error-throwing publisher
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


A try operator that turns this can use either sink(receiveValue:) or
into an error-throwing pipeline. assign(to:).

www.bigmountainstudio.com 387 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

Try adding an assign(to:) subscriber. If Xcode


All operators that begin with So far, the only publisher I know that can
gives you an error, then usually something is throwing an
“try“ throw errors. throw an error is the dataTaskPublisher.

OPTION+click a publisher/operator and view the help documentation


and look for words like “throw” or “error”.

(Decode operator)

www.bigmountainstudio.com 388 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 390 Combine Mastery in SwiftUI


Handling Errors

AssertNoFailure - View Model


class AssertNoFailure_IntroViewModel: ObservableObject {
If you run this code as it is, Xcode will halt execution and display this error:
@Published var dataToView: [String] = []

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
You have seen from the many examples where a try operator is used that Xcode
dataToView.append(item)
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 391 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()
}
}
When fetching data the pipeline
} encounters invalid data and throws an
error. The catch intercepts this and
publishes “Error Found”.

www.bigmountainstudio.com 393 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 == "🧨 " {
throw BombDetectedError() Use width: 214
}
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)
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.

www.bigmountainstudio.com 394 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 396 Combine Mastery in SwiftUI



Handling Errors

TryCatch - View Model


class TryCatch_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = [] struct BombDetectedError: Error, Identifiable {
@Published var error: BombDetectedError? 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 {
self.error = error as? BombDetectedError When fetch tries to get data it runs into a problem, throws an
} 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
fetchAlternateData.
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()
}
}

www.bigmountainstudio.com 397 Combine Mastery in SwiftUI


MapError
!
error
" $
error
#
error

try try 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()
}
If the pipeline throws any
Use width: 214 List(vm.todos) { todo in errors then the
Label(title: { Text(todo.title) }, mapError will receive
icon: { Image(systemName: todo.completed ? and republish another
"checkmark.circle.fill" : error.
"circle") })
} It will be assigned to this
} error published
.font(.title) property.
.alert(item: $vm.error) { error in
Alert(title: Text("Error"), message: Text(error.message)) When the alert modifier
} detects a value, it will
} present an alert to the
} user.

www.bigmountainstudio.com 399 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 {


struct ToDo: Identifiable, Decodable {
@Published var todos: [ToDo] = [] var id: Int
@Published var error: ErrorForView? var title: String
var completed: Bool
struct ErrorForView: Error, Identifiable {
}
let id = UUID()
private var cancellable: AnyCancellable?
var message = ""
}
func fetch() {
let url = URL(string: "https://jsonplaceholder.typicode.com/users/1/todos")! Using the dataTaskPublisher for this example
because it can throw an error and we can
throw more errors depending on the
cancellable = URLSession.shared.dataTaskPublisher(for: url)
response.
.tryMap { (data: Data, response: URLResponse) -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw UrlResponseErrors.unknown
} Check the response codes to see if there were any problems and
if (400...499).contains(httpResponse.statusCode) { throw an error. Here is the error object:
throw UrlResponseErrors.clientError
enum UrlResponseErrors: String, Error {
}
case unknown = "Response wasn't recognized"
if (500...599).contains(httpResponse.statusCode) { case clientError = "Problem getting the information"
throw UrlResponseErrors.serverError case serverError = "Problem with the server"
} case decodeError = "Problem reading the returned data"
}

return data
}
.decode(type: [ToDo].self, decoder: JSONDecoder()) Note: The decode operator can also throw an error.

www.bigmountainstudio.com 400 Combine Mastery in SwiftUI


Handling Errors

You can see that mapError receives an error and the


.mapError { error -> UrlResponseErrors in
closure is set to ALWAYS return a UrlResponseErrors
if let responseError = error as? UrlResponseErrors {
type. (See the previous page for this object.)
return responseError
} else { So mapError can receive many different types of errors
return UrlResponseErrors.decodeError and you control the type that gets sent downstream.
}
} If there is an error that enters the sink subscriber, you
.receive(on: RunLoop.main) already know it will be of type UrlResponseErrors
.sink { [unowned self] completion in because that is what the mapError is returning:
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
The receive operator switches execution back to the
remember, the dataTaskPublisher could also throw an error.
main thread. If you don t do this, Xcode will show you a
purple warning and you may or may not see results
So if you do use mapError, be sure to check the type of the error received so
appear on the UI.
you know where it s coming from before changing it in some way.

www.bigmountainstudio.com 401 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 .foregroundStyle(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 403 Combine Mastery in SwiftUI


Handling Errors

ReplaceError - View Model


class ReplaceError_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = []
var replacedValue = "Error Found"

func fetch() {
You will not see these values published
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"] because the pipeline will finish after

_ = dataIn.publisher
.tryMap{ item in

if item == "🧨 " { struct BombDetectedError: Error, Identifiable


{
throw BombDetectedError() let id = UUID()
} }
return item
}
.replaceError(with: replacedValue)
Notice you do not have to use sink(receiveCompletion:receiveValue:). This is
.sink { [unowned self] (item) in
because replaceError turned the pipeline into a non-error-throwing pipeline.
dataToView.append(item)
}
}
}

www.bigmountainstudio.com 404 Combine Mastery in SwiftUI


Retry
failure?

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) The webPage property will either show the HTML it retrieved
from a website or an error message.
Use width: 214 .padding()

Spacer(minLength: 0)
}
.font(.title)
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 406 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


cancellable = URLSession.shared.dataTaskPublisher(for: url)
actually get run 3 times.
.retry(2)
The publisher runs the first time, fails, then runs 2
.map { (data: Data, response: URLResponse) -> String in more times to retry.
String(decoding: data, as: UTF8.self)
}
The receive operator switches execution back to the
.receive(on: RunLoop.main)
main thread. If you don t do this, Xcode will show you a
.sink(receiveCompletion: { [unowned self] completion in purple warning and you may or may not see results
if case .failure(_) = completion { appear on the UI.
webPage = "We made 3 attempts to retrieve the webpage and failed."
}
}, receiveValue: { [unowned self] html in
webPage = html
})
}
}

www.bigmountainstudio.com 407 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 410 Combine Mastery in SwiftUI


Debugging

Breakpoint - View Model


class Breakpoint_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = [] You don t need to include all three parameters
(closures). Just use the ones you want to examine.
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)
})
}
} Xcode Debugger Console

www.bigmountainstudio.com 411 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. At this point, I would find where it was thrown
You can click on the next item with the purple icon (13) to see which file threw the and then add Xcode breakpoints to more closely
SIGTRAP and go from there. examine the code.

www.bigmountainstudio.com 412 Combine Mastery in SwiftUI




BreakpointOnError
1
2
3
! 4
error 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
.font(.title) gets what it considers invalid data.
.onAppear {
During development, if you get invalid data, you
vm.fetch()
want to tell your data team that it needs to be
} corrected before releasing the app.
}

} So you can use breakpointOnError to your


pipeline to warn you of invalid data (or whatever
else you don t expect).

www.bigmountainstudio.com 414 Combine Mastery in SwiftUI



Debugging

BreakpointOnError - View Model


class BreakpointOnError_IntroViewModel: ObservableObject {
@Published var dataToView: [String] = [] Your assumption is that this If an error is thrown, Xcode will pause
should never happen. If it does execution and show you this window. I
happen, Xcode will pause recommend looking at the stack trace to
func fetch() {
execution with the debugger. find where it originated from.
let dataIn = ["Mercury", "Venus", "Earth", "Pluto"]

_ = 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 415 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 417 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
func fetch() { ones you want.
let dataIn = ["Mercury", "Venus", "Earth", "Pluto"]

_ = dataIn.publisher
.handleEvents(
receiveSubscription: { subscription in
print("Event: Received subscription")
}, receiveOutput: { item in
print("Event: Received output: \(item)")
Note: The receiveCompletion in this
}, receiveCompletion: { completion in
print("Event: Pipeline completed") example will not execute because there is an
}, receiveCancel: { error is being thrown (Pluto).
print("Event: Pipeline cancelled")
}, receiveRequest: { demand in
print("Event: Received request")
})
.tryMap { item in
if item == "Pluto" { You can see the output for the events
throw InvalidPlanetError()
}
return item
}
.sink(receiveCompletion: { completion in
print("Pipeline completed")
}, receiveValue: { [unowned self] item in
dataToView.append(item)
})
}
}
Xcode Debugger Console

www.bigmountainstudio.com 418 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 }
}
}
}
The handleEvents operator sets the
if vm.isFetching { isFetching property.
ProcessingView()
}
Note: You can see the code for
} ProcessingView here.
.font(.title)
.onAppear {
vm.fetch()
}
}
}

www.bigmountainstudio.com 419 Combine Mastery in SwiftUI


Debugging

HandleEvents for Showing Progress - View Model


class HandleEvents_ProgressViewModel: ObservableObject {
This is the struct the JSON is decoding into:
@Published var usdBitcoinRate = ""
@Published var isFetching = false struct BitcoinPrice: Decodable {
let bpi: Bpi
func fetch() {
let url = URL(string: "https://api.coindesk.com/v1/bpi/currentprice.json")! struct Bpi: Decodable {
let USD: Rate
let GBP: Rate
URLSession.shared.dataTaskPublisher(for: url) let EUR: Rate
.map { (data: Data, response: URLResponse) in
data struct Rate: Decodable {
} let rate: String
.decode(type: BitcoinPrice.self, decoder: JSONDecoder()) }
}
.receive(on: RunLoop.main) }
.handleEvents(receiveCompletion: { [unowned self] _ in
isFetching = false
}, receiveCancel: { [unowned self] in
isFetching = false The pipeline could complete normally or be
}, receiveRequest: { [unowned self] _ in canceled so isFetching is set to false in both
isFetching = true
})
.map{ bitcoinPrice in
bitcoinPrice.bpi.USD.rate // Return just the USD rate
}
.catch { _ in
Just("N/A")
} Learn more about these
.assign(to: &$usdBitcoinRate) publishers and operators:
} • dataTaskPublisher
} • catch

www.bigmountainstudio.com 420 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() Simply add print() to start printing all
.replaceNil(with: "<Needs ID>") events related to this pipeline to the
.sink { [unowned self] datum in
debug console.
self.data.append(datum)
}
}
}

struct Print_Intro: View {


@StateObject private var vm = UsingPrint_IntroViewModel()
Use width: 214
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 422 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 de-
initialize. 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 424 Combine Mastery in SwiftUI


Debugging

Testing for Memory Leaks - View Model


class TestingMemory_ViewModel: ObservableObject {
Add a deinit function to your view model.
@Published var data = ""
This function is called right before the
class is removed from memory.
func fetch() {
If it does not get run, you know you
data = "New value"
have a memory leak.
}

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 425 Combine Mastery in SwiftUI


MORE RESOURCES

Big Mountain Studio creates premium visual reference materials. This means the 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 By Joseph Heck
examples
By Donny Wals This book explains the core concepts, provides
examples and sample code, and provides a
Learn Combine from the ground up with a solid reference to the variety of tools that Combine
theoretical foundation and real-world examples of makes available under its umbrella.
where and how Combine can help you move from
writing imperative code to writing reactive code
that is flexible, clean and modern.

A Combine Kickstart Combine


By Daniel Steinberg Asynchronous Programming with Swift
By Florent Pillet, Shai Mishali, Scott Gardner,
This hand-on, fast-moving kickstart introduces you Marin Todorov
to the future of declarative and reactive
programming on Apple platforms. We focus on Writing asynchronous code can be challenging, with
core concepts and building discrete, easy-to- a variety of possible interfaces to represent,
understand, pieces of a pipeline that allows your perform, and consume asynchronous work —
app to react to changes in the state. 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 427 Combine Mastery in SwiftUI


More Resources

Learning From Others - Video Courses

Code With Chris Combine Framework Course


A Swifty Combine Framework Course
Chris has a variety of SwiftUI content as well as By Karin Prater
UIKit, Xcode tips and tricks, GitHub, Bitrise,
CoreML, Concurrency, design, pretty much Master Combine with great coding examples in
everything a developer would need and then some UIKit and SwiftUI. Discover all the tools you need
to get a high paying developer job. 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 428 Combine Mastery in SwiftUI


THE END

Good job!
You Get 20% Off!
Take Advantage of Your Loyalty Discount!

Because you bought this book you get the loyalty discount of 20% off everything in the store. This is to encourage you to
continue your SwiftUI journey and keep getting better at your craft. Enter the code above on checkout or click the button
below. 👇

ACTIVATE DISCOUNT

(Note: You should have gotten an email with this coupon code too. Make sure you opt-in for even better discounts through sales in the future.
Go here for instructions on how to get notified for the biggest savings.)

430
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 www.bigmountainstudio.com/makemoney and sign up. You will need a PayPal account to get paid.
More From Me

SwiftUI Essentials
ARCHITECTING SCALABLE & MAINTAINABLE APPS

Working with data in SwiftUI is super confusing. I know, I was there


trying to sort it all out. That s why I made this simple to read book
so that anyone can learn it.
How to architect your app
Learn what binding is
What is @StateObject and when should you use it?
How is @State different from @StateObject?
How can you have data update automatically from parent to
child views?
How can you work with a data model and still be able to
preview your views while creating them?
How do you persist data even after your app shuts down?

SAVE 20% ON THIS BOOK!

432

More From Me

SwiftUI Views
THE FASTEST WAY TO LEARN SWIFTUI IS WITH PICTURES!

SAVE 20% ON THIS BOOK!

Over 1,000 pages of SwiftUI Find out how to implement action sheets, modals, popovers
Over 700 screenshots and video showing you what you can and custom popups
do so you can quickly come back and reference the code Master all the layout modifiers including background and
Learn all the ways to work with and modify images overlay layers, scaling, offsets padding and positioning
See the many ways you can use color as views How do you hide the status bar in SwiftUI? Find out!
Discover the different gradients and how you can apply them This is just the tip of the mountain!

433
More From Me

SwiftUI Animations Mastery


DO YOU LIKE ANIMATIONS? WOULD YOU LIKE TO SEE HUNDREDS OF VIDEO ANIMATION EXAMPLES WITH THE CODE?

SwiftUI made animations super easy…except when it isn t. Most new


SwiftUI developers can get a simple animation working but as soon
as they want to customize it, they get lost and waste more time than
they should trying to get it working. This book will help you with that
struggle.
Learn all the animation types and options with embedded video
samples and code
Master spring animations
Master transitions for views that are inserted and removed from
the screen
Learn how matchedGeometryReader should really work
Customize animations with speeds, delays, and durations

SAVE 20% ON THIS BOOK!

434

More From Me

SwiftData Mastery in SwiftUI


QUICKLY LEARN APPLE’S NEW SWIFTDATA FRAMEWORK VISUALLY SO YOU CAN SAVE TIME BUILDING APP FEATURES.
Over 500 pages - The largest SwiftData book for SwiftUI
What are the 4 main concepts that will help you finally
understand how SwiftData works?
How can you start using SwiftData in just 3 steps and
under 10 minutes?
How can you use AI to create a lot of mock data?
How can you make your life much easier when using
SwiftData and SwiftUI?
How can you not only get data, but also sort, filter, and
animate with a query?
How do you sync data across multiple devices?
What is one object you can use to prevent your UI from
hanging and data from getting corrupted when doing a
large number of SwiftData operations?
BONUS: Get 4 SwiftData apps with integrations for
MapKit, PhotosUI, and Charts.

SAVE 20% ON THIS BOOK!

435
More From Me

Core Data Mastery in SwiftUI


QUICKLY LEARN APPLE’S CORE DATA FRAMEWORK VISUALLY SO YOU CAN SAVE TIME BUILDING APP FEATURES.
Over 500 pages - The largest Core Data book for SwiftUI
What are the 4 main concepts that will help you finally
understand how Core Data works?
How can you start using Core Data in just 4 steps and
under 10 minutes?
How can you use mock data in Core Data and preview it
while developing your UI?
How can you make your life much easier when using Core
Data and SwiftUI?
How can you not only get data, but also sort, filter, and
animate with a fetch request?
What can you do if you want to group your data into
sections?
How can you use the managed object context to insert,
update, and delete data?
Much, much more. Click the link to see.

SAVE 20% ON THIS BOOK!

436
More From Me

Combine Mastery in SwiftUI


HAVE YOU TRIED TO LEARN COMBINE AND FAILED LIKE I DID…MULTIPLE TIMES?

I finally figured out the secret to understanding Combine after 2


years and now I m able to share it with you in this visual, game-
changing reference guide.
How can you architect your apps to work with Combine?
Which Swift language topics should you know specifically that
will allow you to understand how Combine works?
What are the important 3 parts of Combine that allows you to
build new data flows?
How can Combine kick off multiple API calls all at one time and
handle all incoming data to show on your screen? Using about
12 lines of code…which includes error handling.

SAVE 20% ON THIS BOOK!

437

You might also like