26335038
26335038
Combine Mastery
iOS 15
In SwiftUI
Mark Moeykens
www.bigmountainstudio.com A COMBINE REFERENCE GUIDE
1 FOR SWIFTUI DEVELOPERS Big Mountain
Combine MasteryStudio
in SwiftUI
Version: 30-DECEMBER-2021
©2021 Big Mountain Studio LLC - All Rights Reserved
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 of
feedback, support, and business guidance: Chris Ching, Scott Smith, Rod information, whether Swift or Combine, that really helped me out and
Liberal, Chase Blumenthal and Chris Durtschi. 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 help in Wals, Karin Prater, Antoine van der Lee, Paul Hudson, Joseph Heck, Vadim
making this book possible. This includes Dave DeLong, Parker Wightman, Bulavin, Daniel Steinberg and Meng To.
TABLE OF CONTENTS
The table of contents should be built into your ePub and PDF readers. Examples:
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
3 be copying code from this book. (You can also get this code from the companion project too.)
Spacer()
}
.font(.title)
}
}
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)
.padding()
3
var body: some View { .background(Color("Gold"))
VStack(spacing: 15) { .foregroundColor(.white)
if !title.isEmpty { }
Text(title) }
.font(.largeTitle) 1
}
Text(subtitle)
2 .foregroundColor(.gray)
DescView(desc)
}
}
}
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.
Embedded Videos
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
[ , , , ]
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.
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.
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"),
BookModel(name: "SwiftUI Animations"),
• Updates to the data it exposes to the view (@Published property, in this example)
BookModel(name: "Data in SwiftUI"), • Logic to validate data (may or may not be in the model)
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
@Published will also be covered later in this book. logic in the View Model to make it easier for you to read and learn and not have to skip
around or flip pages to connect all the dots.
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.
}
}
Many of you, including myself, had to change the way we
.onAppear {
thought about the View.
vm.fetch()
}
You can’t reference UI elements and then access their
}
Use width: 214 properties and update them directly in SwiftUI.
}
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.
Publisher Subscriber
A type that can push out data. It can push out the data all at once Something that can receive data from a publisher.
or over time. In English, “subscribe” means to “arrange to receive
In English, “publish” means to “produce and send out to make something”.
known”.
Publisher Subscriber
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.
Upstream, Downstream
You will also see documentation (and even some types) that mention “upstream” and “downstream”.
Upstream Downstream
“Upstream” means “in the direction of the PREVIOUS part”. “Downstream” means “in the direction of the NEXT part”.
In Combine, the previous part is usually a Publisher or Operator. In Combine, the next part could be another Publisher, Operator
or even the Subscriber at the end.
Upstream Downstream
SwiftUI Combine
In SwiftUI, you start with a View and you can add many modifiers to With Combine, you start with a Publisher and you can add many operators
that View. (modifiers) to that Publisher.
Each modifier returns a NEW, modified View: Each operator returns a NEW, modified operator:
MyStringArrayPublisher
Text("Hello, World!")
.fakeOperatorToRemoveDuplicates()
.font(.largeTitle)
.fakeOperatorToRemoveNils()
.bold()
.fakeOperatorToFilterOutItems(thatBeginWith: “m”)
.underline()
.fakeOperatorToPublishTheseItemsEvery(seconds: 2)
.foregroundColor(.green)
.fakeSubscriberToAssignThisVariable(myResultVariable)
.padding()
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.
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
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.
If you know that a specific protocol always has a “name” property, then it doesn’t matter what class or struct you are working with that uses this protocol, you know
that they will all ALWAYS have a “name” property.
You are not required to know anything else about the class or struct that follows this protocol. There might be a lot of other functions and properties. But because
you know about the protocol that class or struct uses then you also know they are ALL going to have that “name” property.
Protocols
Protocols Introduction
protocol PersonProtocol {
var firstName: String { get set }
By itself, a protocol does nothing and does not
var lastName: String { get set } contain any logic.
Text("Name: \(dev.getFullName())")
}
.font(.title)
}
}
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
init(first: String, last: String) { implements the getFullName function a little
firstName = first differently.
lastName = last
}
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.
}
Protocols allow Publishers (and Operators) to have the same functions and all Subscribers to have the same exact functions too.
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 receive(subscription:) func receive(input:) func receive(completion:)
Most likely you will never have to create a class that conforms to these Let’s learn more about how that works in
protocols in your career with Combine. the next section…
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.
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)")
// }
Generics On Objects
struct Generic_Objects: View {
init(myProperty: T) {
self.myProperty = myProperty
}
}
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).
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
}
}
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)
}
}
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."
}
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.
(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.
You use the associatedtype keyword. This is something the Publisher and Subscriber protocols make use of.
associatedtype & typealias
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)
}
}
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
return "Team one wins" developer only uses a certain category of types that, for
example, match a certain protocol.
}else if teamOne == teamTwo {
return "The teams tied."
}
return "Team two wins"
}
}
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
var team2: Team { get set } generic constraints by using the colon followed
by the type.
Constraints - View
struct AssociatedType_Constraints: View {
@State private var comparison = ""
private let weekendGame = WeekendGame()
Button("Evaluate Teams") {
comparison = weekendGame.compareTeamSizes() Use width: 214
}
Text(comparison)
Spacer()
}
.font(.title)
}
}
As you know, Combine has a protocol for the Publisher and the Subscriber. Both protocols define inputs, outputs, and failures using associated types.
Publishers can publish any type you want for its output. Output could Subscribers can receive any input from the connected publisher.
be simple types like String, Int, or Bools or structs of data you get The Failure generic is constrained to the Error protocol.
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.
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.
struct Int
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…
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
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.
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.
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.
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.
Template
The code between the ObservableObject and View might look something like this:
• 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.
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.
}
Text(vm.state)
Sequence
init() {
// Change the name value after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = "2. Second State"
}
}
}
HStack {
TextField("name", text: $vm.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: vm.name, perform: { value in
message = value.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!
}
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!
}
The Pieces
Your pipeline always starts with a publisher
and always ends with a subscriber.
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!
$name
So what is this Publisher?
}
} Apple says it is: “A publisher for
properties marked with the
@Published attribute.”
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
OK, we have a publisher and an operator. We still need the third piece, the subscriber.
London 51° N 0° W
Paris 48° N 2° E
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.
@Published
Property
Note: The assign(to: ) ONLY works
with @Published properties.
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 the
value = value * 2
}
that says: dollar sign like this:
var y = 4
doubleThis(value: &y) Hey, this function can let namePublisher = vm.$name
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).
let vm = ViewModel()
print(vm.message)
$message
(Playgrounds output)
The Publisher
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.
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.
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!"
} Don’t do this! 😃
.assign(to: &$message)
}
}
let vm = ViewModel()
print(vm.message)
What’s happening?
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.
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
a property. Property
You access the Publisher down the pipeline and
part of the @Published can evaluate and run
property by using the logic on them. This particular function
dollar sign ($). can ONLY work with
When it’s done, it sends @Published properties.
the new value
downstream through the
pipeline.
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
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.
Data
your sink.
Data
Da
ta
ta
Da
Data
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 ? "❌ " : "✅ " } init() {
From this point on, you will
cancellable = $name
.assign(to: &$validation) need to import Combine for
} all of your view models. .map { $0.isEmpty ? "❌ " : "✅ " }
} .sink { [unowned self] value in
self.validation = value
}
}
}
This class conforms to the Cancellable protocol which has just public protocol Cancellable {
one function, cancel(). func cancel()
}
The warning should also tell you that your pipeline will immediately be cancelled after init completes!
Run once?
If you only want to run the pipeline one time and not show the warning init() {
then use the underscore like this: _ = $name
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
(The underscore just means you are not using the result of the function.)
self.validation = value
}
But be warned, if you have an operator that delays execution, the pipeline }
The View
struct FirstPipelineUsingSink: View {
@StateObject private var vm = FirstPipelineUsingSinkViewModel()
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
}
When you play this video, notice that private and instead expose a public
after cancelling the subscription, the function you can call. See next page for an
validation no longer happens. example of this…
func cancel() {
status = "Cancelled"
cancellablePipeline?.cancel() The cancelling functionality is now in a public cancel
// OR function that the view can call.
cancellablePipeline = nil
}
}
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)
}
}
Unowned Self
In many of these code examples you see me using [unowned self]. Why?
Pipeline Lifecycle
Is [unowned self] better than [weak self]?
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()
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…
}
}
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()
init() {
$firstName
.map { $0.isEmpty ? "❌ " : "✅ " }
.sink { [unowned self] value in
self.firstNameValidation = value
}
.store(in: &validationCancellables)
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
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.
$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()
TextEditor(text: $vm.data)
.border(Color.gray, width: 1)
.frame(height: 200)
.padding()
Text("\(vm.characterCount)/\(vm.characterLimit)")
.foregroundColor(vm.countColor)
}
Combine is being used to produce the
.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.
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
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.
You can send in the value directly into the initializer too.
(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.)
CurrentValueSubject - View
struct CurrentValueSubject_Intro: View {
@StateObject private var vm = CurrentValueSubjectViewModel()
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)
.foregroundColor(vm.selectionSame.value ? .red : .green)
}
.font(.title)
}
}
Notice that you have to access the value property to
read the publisher’s underlying value.
View Model
In the view model (which you will see on the next page) the selection property is declared as a CurrentValueSubject like this:
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, making it
vm.selection.value = "Ellen"
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.”
init() {
selection
This will NOT work.
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 to
selectionSame.value = value 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.
Sequence of Events
CurrentValueSubject @Published
Let’s see how the same UI and view model would work if we used
@Published properties instead of a CurrentValueSubject publisher.
Button("Select Lorenzo") {
vm.selection = "Lorenzo"
The view model for this view is using a
@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)
.foregroundColor(vm.selectionSame.value ? .red : .green)
}
.font(.title)
}
}
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
.sink { [unowned self] value in 2. The value is set
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)
}
}
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()
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()
}
}
}
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.
}
dataToView.append(item)
!
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 = ""
Text("\(vm.age)")
}
.font(.title)
.alert(item: $vm.error) { error in
Alert(title: Text("Invalid Age"), message: Text(error.rawValue))
}
}
}
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
The type you want to pass down the This is the error that could be sent to the subscriber if something goes
pipeline in the future to the wrong. Never means the subscriber should not expect an error/failure.
subscriber. Otherwise, you can create your own custom error and set this type.
You want to call this function at some point in the future’s In this example, a String is being
closure. assigned to the success case.
(Note: If any of this use of generics is looking unfamiliar to you, then take a look at the chapter on Generics and how they are used with Combine.)
Future - View
struct Future_Intro: View {
@StateObject private var vm = Future_IntroViewModel()
Button("Say Hello") {
vm.sayHello()
}
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.
}
func sayHello() { Because Future is declared with no possible failure (Never), this becomes a non-error-
Future<String, Never> { promise in 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 variable.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 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 soon
goodbye = message
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.
}
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.
}
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.
}
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.
}
}
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)
}
}
func fetch() {
futurePublisher
.assign(to: &$firstResult)
}
func runAgain() {
futurePublisher
.assign(to: &$secondResult)
}
}
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>))
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 - View
struct Just_Introduction: View {
@StateObject private var vm = Just_IntroductionViewModel()
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
.assign(to: &$data) pipeline.
}
After Just publishes the one item, it will finish
} the pipeline.
}
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()
HStack {
TextField("credit card number", text: $vm.creditCard)
Group {
switch (vm.status) {
case .ok:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
case .invalid:
Image(systemName: "x.circle.fill")
A PassthroughSubject is a good
.foregroundColor(.red)
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") {
vm.verifyCreditCard.send(vm.creditCard) button is tapped.
}
}
.font(.title)
} Like the CurrentValueSubject, you
}
have access to a send function that will
send the value through your pipeline.
There are types in Swift have built-in publishers. In this section, you will learn about the Sequence publisher which sends elements of a collection through a pipeline
one at a time.
Once all items have been sent through the pipeline, it finishes. No more items will go through, even if you add more items to the collection later.
Publishers
Sequence - View
struct Sequence_Intro: View {
@StateObject private var vm = SequenceIntroViewModel()
func fetch() {
var dataIn = ["Paul", "Lem", "Scott", "Chris", "Kaya", "Mark", "Adam", "Jared"]
// Process values
dataIn.publisher
.sink(receiveCompletion: { (completion) in
print(completion)
(Xcode Debugger Console)
}, receiveValue: { [unowned self] datum in
self.dataToView.append(datum)
print(datum)
})
.store(in: &cancellables)
This means the array is passed into the publisher and the
publisher iterates through all items in the array (and then
the publisher finishes).
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()
Text("Adjust Interval")
Slider(value: $vm.interval, in: 0.1...1,
minimumValueLabel: Image(systemName: "hare"),
maximumValueLabel: Image(systemName: "tortoise"),
label: { Text(“Interval") })
.padding(.horizontal)
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()
start() set .main to have this run on
} the main thread.
}
The last parameter is the
func start() {
RunLoop mode.
timerCancellable = Timer
.publish(every: interval, on: .main, in: .common)
(Run loops manage events and
.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.
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)
}
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.
}
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.
Download task
URLSession
Upload task
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
• Great for simple tasks like fetching data from a URL to memory • You can change the default request and response timeouts
• You can’t obtain data incrementally as it arrives from the server • You can make the session wait for connectivity to be established
• You can’t customize the connection behavior • You can prevent your app from using a cellular network
• Your ability to perform authentication is limited • Add additional HTTP headers to all requests
• You can’t perform background downloads or uploads when your app • Set cookie, security, and caching policies
isn’t running • Support background transfers
• You can’t customize caching, cookie storage, or credential storage • See more options here.
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
The data is what is returned from the URL The response is like the status of how the If there was some problem with trying to
you provided to the DataTaskPublisher. call to the URL went. Could it connect? Was connect and get data then an error is
Note: What is returned is represented as it successful? What kind of data was thrown.
bytes in memory, not text or an image. returned?
DataTaskPublisher - View
struct UrlDataTaskPublisher_Intro: View {
@StateObject private var vm = UrlDataTaskPublisher_IntroViewModel()
This URL I’m using returns some cat facts. Let’s see how
the pipeline looks on the next page.
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 {
@Published var dataToView: [CatFact] = [] Map
var cancellables: Set<AnyCancellable> = [] And that is what we are doing with the map operator. The map receives
the tuple but then republishes only one value from the tuple.
(Note: The return keyword was made optional in Swift 5 if there is only one
func fetch() {
thing being returned. You could use return data if it makes it more clear
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
for you.)
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in
data
Can it be shorter?
Yes! I wanted to start with this format so you can explicitly see the tuple
}
coming in from the dataTaskPublisher.
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
To make this shorter you can use what’s called “shorthand argument
.sink(receiveCompletion: { completion in
names” or “anonymous closure arguments”. It’s a way to reference
print(completion) arguments coming into a closure with a dollar sign and numbers:
}, receiveValue: { [unowned self] catFact in
dataToView = catFact $0 = (data: Data, response: URLResponse)
}) The $0 represents the tuple.
.store(in: &cancellables)
} Using shorthand argument names, you can write the map like this:
}
.map { $0.data }
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
@Published var dataToView: [CatFact] = [] endpoint. It is just a bunch of bytes in memory that could
represent different things like text or an image. In order to use
var cancellables: Set<AnyCancellable> = []
data, we will have to transform or decode it into something else.
func fetch() {
Decode
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! The decode operator not only decodes those bytes into something
URLSession.shared.dataTaskPublisher(for: url) we can use but will also apply the decoded data into a type that
.map { $0.data } you specify.
DataTaskPublisher - Receive(on: )
struct CatFact: Decodable {
let _id: String Asynchronous
let text: String The dataTaskPublisher will run asynchronously. This means that 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
class UrlDataTaskPublisher_IntroViewModel: ObservableObject {
background, the user can still use your app and it’ll be responsive in the
@Published var dataToView: [CatFact] = [] foreground.
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
func fetch() {
foreground.
let url = URL(string: "https://cat-fact.herokuapp.com/facts")!
URLSession.shared.dataTaskPublisher(for: url) We call the background and foreground “threads” in memory.
.map { $0.data }
.decode(type: [CatFact].self, decoder: JSONDecoder()) Thread Switching
.receive(on: RunLoop.main) To move data that is coming down your background pipeline to a new
foreground pipeline, you can use the receive(on:) operator.
.sink(receiveCompletion: { completion in
It basically is saying, “We are going to receive this data coming down the
print(completion) pipeline on this new thread now.” See section on receive(on:).
}, receiveValue: { [unowned self] catFact in
dataToView = catFact Scheduler
}) You need to specify a “Scheduler”. A scheduler specifies how and where
work will take place. I’m specifying I want work done on the main thread.
.store(in: &cancellables)
(Run loops manage events and work. It allows multiple things to happen
}
simultaneously.)
}
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 one
var cancellables: Set<AnyCancellable> = [] 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 and the
let url = URL(string: "https://cat-fact.herokuapp.com/facts")! decode operator can throw an error.
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data } Xcode’s autocomplete won’t even show you the first sink option for this
pipeline so you don’t have to worry about which one to pick.
.decode(type: [CatFact].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
Handling Errors
.sink(receiveCompletion: { completion in
There are many different ways you can handle errors that might be
print(completion) thrown using operators or subscribers. For more information on options,
}, receiveValue: { [unowned self] catFact in 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 an alert on the view
.store(in: &cancellables) using another example on the next page.
}
}
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))
}
}
}
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.
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 the This is a good indication that the operator will allow you to
browser the expected response. 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
https://... ( , )
This section will show you an example of how to use the DataTaskPublisher to get an image using a URL.
Publishers
vm.imageView
}
Use width: 214 .font(.title)
.onAppear {
vm.fetch()
}
.alert(item: $vm.errorForAlert) { errorForAlert in
Alert(title: Text(errorForAlert.title),
message: Text(errorForAlert.message))
In this example, the Big Mountain
Studio logo is being downloaded
}
using a URL.
}
} If there’s an error, the alert modifier
will show an Alert with a message to
the user.
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
if case .failure(let error) = completion { then an error will be thrown
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.
}
vm.imageView
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.
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.
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()
func add(number: String) { Note: You may also notice that I’m using Shorthand Argument Names
numbers.publisher here instead of $numbers. Here is an alternative way to write this using
if number.isEmpty { return }
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.
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()
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
vm.add(number: number) numbers are Fibonacci numbers but
if any 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.
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 error
self.invalidNumberError = error as? InvalidNumberError 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
}
}
== 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()
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()
}
}
}
func fetch() {
let incomingData = ["3 bedrooms", "2 bathrooms", "Air conditioning", "Basement"]
incomingData.publisher
.prefix(2) The prefix operator just returns
.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
Can I use contains on my custom data objects?
.contains("Basement")
.assign(to: &$basement) If they conform the Equatable protocol you can. The
} Equatable protocol requires that you specify what determines if
} two of your custom data objects are equal. You may also want to
look at the contains(where: ) operator on the next page.
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.
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()
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
fruitName = fruit.name
property because I don’t need to. After the pipeline
} finishes, I don’t have to hold on to a reference of it.
incomingData.publisher
.contains(where: { (fruit) -> Bool in
fruit.nutritionalInformation.contains("Vitamin A") 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)
}
}
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()
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.
}
}
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()
}
return item == place But if an item from your data source contains the
}) 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
}
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()
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. Use width: 214
.assign(to: &$count)
}
}
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()
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.
}
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.
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()
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.city)
Use width: 214 .foregroundColor(.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()
}
}
}
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 }
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
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.country)
Use width: 214 .foregroundColor(.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()
}
}
}
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")]
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()
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.
}
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)
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()
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Use width: 214 Text(profile.city)
.foregroundColor(.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()
}
}
}
Well, you’re not actually specifying the criteria to get the min
profiles = dataIn value, instead, you’re specifying the ORDER so that
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
return currentItem.city < nextItem.city define how these items should be ordered and that’s 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:
}
.max { $0.city < $1.city }
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()
List(vm.profiles) { profile in
Text(profile.name)
.frame(maxWidth: .infinity, alignment: .leading)
Text(profile.country)
Use width: 214 .foregroundColor(.secondary)
}
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.
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”
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
self.dataToView.append(datum) all other items finish.
}
}
}
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.")
Let’s take a closer look at the view model on the next page.
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"]
?
}
See how this is done in the view model on the next page…
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)
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()
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.
}
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.
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.
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()
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)
.sink { [unowned self] datum in know the first two items in the data
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.")
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()
Text("Limit Results")
Slider(value: $vm.itemCount, in: 1...10, step: 1)
Text(“\(Int(vm.itemCount))")
Button("Fetch Data") {
vm.fetch()
}
Spacer(minLength: 0)
}
.font(.title)
}
}
func fetch() {
data.removeAll()
let fetchedData = ["Result 1", "Result 2", "Result 3", "Result 4", "Result 5", "Result
_ = 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.
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.
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()
Button("Open Pipeline") {
vm.startPipeline.send()
}
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)
}
}
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)
}
}
}
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
func fetch() { prepend operator will just run
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)
}
}
}
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)
}
}
}
See how this is done in the view model on the next page…
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)
“UNREAD”
“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.
Combine gives you operators that you can use to control the timing of data delivery. Maybe you want to delay the data delivery. Or when you get too much data, you
can control just how much of it you want to republish.
Debounce
orld! Hello, W
Think of “debounce” like a pause. The word “bounce” is used in electrical engineering. It is when push-button switches make and break contact several times when
the button is pushed. When a user is typing and backspacing and typing more it could seem like the letters are bouncing back and forth into the pipeline.
The prefix “de-” means “to remove or lessen”. And so, “debounce” means to “lessen bouncing”. It is used to pause input before being sent down the pipeline.
Operators
Debounce
class DebounceViewModel: ObservableObject {
@Published var name = "" Pipeline: The idea here is that we
@Published var nameEntered = "" want to “slow down” the input so
we publish whatever came into
init() {
the pipeline every 0.5 seconds.
$name
.debounce(for: 0.5, scheduler: RunLoop.main)
.assign(to: &$nameEntered)
}
} The scheduler is basically a
mechanism to specify where and
struct Debounce_Intro: View {
@StateObject private var vm = DebounceViewModel()
how work is done. I’m specifying 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.")
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.
Debounce Flow
eykens Mark Mo
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()
Text("Delay for:")
Picker(selection: $vm.delaySeconds, label: Text("Delay Time")) {
Text("0").tag(0)
Text("1").tag(1)
Text("2").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
Button(“Fetch Data") {
vm.fetch()
}
if vm.isFetching {
ProgressView() A ProgressView will be shown while the data is being
} else { fetched. This is done in the view model shown on the
Text(vm.data) next page.
}
Spacer()
}
.font(.title)
}
}
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
.nanoseconds
on the view.
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
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!
cancellable = timeEvent
.measureInterval(using: RunLoop.main)
.sink { [unowned self] (stride) in The measureInterval will republish a
speed = stride.timeInterval Stride type which is basically a form of
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).
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
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)
Button("Reset") { vm.reset() } turning the pipeline on or off.
.frame(maxWidth: .infinity)
}
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()
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))
}
}
}
These operators give you ways to decide which items get published and which ones do not.
CompactMap
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()
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 from If you don’t have any logic then you can use
the closure but they do not continue down the pipeline. shorthand argument names like this:
.compactMap { $0 }
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()
_ = 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 in
if case .failure(let error) = completion { 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.
}
}
❌ ✅ ❌ ⭐ ✅✅ ✅ ✅ ✅ ✅
== ?
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()
HStack(spacing: 40.0) {
Button("Animals") { vm.filterData(criteria: "Animal") }
Button("People") { vm.filterData(criteria: "Person") }
Button("All") { vm.filterData(criteria: " ") }
}
.filter { $0.contains(criteria) }
❌ ✅ ❌ ⭐ ✅✅ ✅ ✅ ✅ ✅
== ?
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()
HStack(spacing: 40.0) {
Button("Animals") { vm.filterData(criteria: "Animal") }
Button("People") { vm.filterData(criteria: "Person") }
Use width: 214 Button("All") { vm.filterData(criteria: " ") }
}
let dataIn = ["Person 1", "Person 2", "Animal 1", "Person 3", "Animal 2", "Animal 3", "🧨 "]
private var cancellable: AnyCancellable?
init() {
filterData(criteria: " ")
}
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.
}
==
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()
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 {
VStack(spacing: 20) {
HeaderView("RemoveDuplicates(by: )",
subtitle: "Introduction",
desc: "Combine provides you a way to remove duplicate objects that do not
.layoutPriority(1)
Use width: 214
List(vm.dataToView) { item in
Text(item.email)
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:
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()
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
if (previousUserId.email == "N/A" && currentUserId.email == "N/A") { subscriber will catch it and assign it to 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.
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()
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.
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
.replaceEmpty(with: noResults) the 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.
}
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()
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:
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.
}
var fullname = ""
?
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
.sink { [unowned self] (name) in Note: You can also used a shorthand argument find it.
dataToView.append(name) name too: .map { $0.fullname }
The map operator will use these
}
directions to find the property, get the
}
value, and then send that value
}
downstream.
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()
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)
}
}
}
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 - 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.
SetFailureType - View
struct SetFailureType_Intro: View {
@StateObject private var vm = SetFailureType_IntroViewModel()
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
List(vm.states, id: \.self) { state in different publishers are used to get the states.
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))
}
}
}
_ = 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:). )
}
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 {
VStack(spacing: 20) {
HeaderView("Scan",
subtitle: "Introduction",
desc: "The scan operator allows you to access the previous item that it
had returned.")
.font(.title) In this example, I am connecting the current item coming through the
.onAppear { pipeline with the previous item. Then I publish that as a new 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.
_ = dataIn.publisher
dataToView.append(item)
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()
_ = 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)
}
}
}
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()
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)
}
}
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
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.
}
createTeams(with: Int(teamSize))
}
_ = 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.
}
}
}
[ ] [ ] [ ] [ ]
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
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.
init() {
$timeInterval
.sink { [unowned self] _ in fetch() }
Every time timeInterval changes
.store(in: &cancellables) (slider moves), call fetch().
}
func fetch() { Since the fetch function will get called repeatedly
collections.removeAll() as the slider is moving, I’m canceling the pipeline
timerCancellable?.cancel()
Use width: 214
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
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
collections.append(collection)
}
This is where you specify the count.
}
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()
func fetch() {
_ = dataIn.publisher
dataToView.append(item)
}
Use width: 214
_ = dataIn.publisher
As you can see, all the
values never made it
.ignoreOutput()
through the pipeline
.sink(receiveCompletion: { [unowned self] completion in because they were
dataToView2.append("Pipeline Finished") ignored.
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()
func fetch() {
let dataIn = ["elephant", "deer", "mouse", "hippopotamus", "rabbit", "aardvark"]
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()
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
if nextName.contains("tree") { alert modifier on the view:
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. So
self.error = error as? NotAnAnimalError
} a sink with just receiveValue will not work. The error should be handled
} receiveValue: { [unowned self] longestName in in some way so the sink’s completion will assign it to a published property
longestAnimalName = longestName to be shown on the view.
}
}
}
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()
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()
}
}
}
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
.first() pipeline will finish right after that, we can use the
.assign(to: &$firstGuest) assign(to:) subscriber and set the published property.
}
}
==
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()
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.
}
}
}
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) }
==
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()
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
}
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()
Text("Your Destination:")
The last operator is being used to get the last city
Text(vm.destination)
in the user’s list of destinations.
.bold()
func fetch() {
itinerary = ["Salt Lake City, UT", "Reno, NV", "Yellowstone, CA"]
itinerary.publisher
.last() The last operator will just return one item when the
pipeline finishes. Because of that, we can use the
.replaceEmpty(with: "Enter a city")
assign(to:) subscriber and set the published property.
.assign(to: &$destination)
}
There are no try operators or anything else that can
} throw an error so we don’t need a subscriber for
handling pipeline failures.
==
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()
func fetch() {
let dataIn = [Alien(name: "Matt", gender: "man", planet: "Mars"),
Alien(name: "Alex", gender: "non-binary", planet: "Venus"),
Alien(name: "Rod", gender: "man", planet: "Earth"),
Alien(name: "Elaf", gender: "female", planet: "Mercury"),
Alien(name: “Max", gender: "non-binary", planet: "Jupiter"),
Alien(name: "Caleb", gender: "man", planet: "Earth"),
Alien(name: "Ellen", gender: "female", planet: "Venus")] Specify criteria in the closure and after the pipeline finishes, the
last of whatever is remaining will be published.
dataIn.publisher
.last(where: { alien in
alien.gender == "man" && alien.planet == "Earth"
})
.map { $0.name } 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" }
==
The tryLast(where:) operator works just like last(where:) except it also has the ability to throw errors from within the closure provided. If an error is thrown,
the pipeline closes and finishes.
Any try operator marks the downstream pipeline as being able to fail which means that you will have to handle potential errors in some way.
Operators
LastTry(where:) - View
struct TryLast_Where: View {
@StateObject private var vm = TryLast_WhereViewModel()
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)
.foregroundColor(.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()
}
}
}
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
}
}
}
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()
Text("Smart Animals")
.bold()
List(vm.animals, id: \.self) { animal in
Text(animal)
}
}
.font(.title)
}
}
init() {
animals.publisher
Once the right item at the index is found, the
.output(at: index) pipeline finishes and sets the value to the
.assign(to: &$selection) published property.
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 {
VStack(spacing: 20) {
HeaderView("Output(in: )",
subtitle: "Introduction",
desc: "Use output(in:) operator to have your pipeline narrow down its
.padding(.horizontal)
.padding(.horizontal)
.font(.title)
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)
$endIndex
If the end index becomes less than the start index, the app will crash. But
.map { [unowned self] index in
if the end index becomes greater than the number of items that come
index < startIndex ? startIndex : index
through the pipeline you are safe.
}
.sink { [unowned self] index in (Note: You could also control this on the UI or with other methods.)
getAnimals(between: startIndex, end: index)
}
.store(in: &cancellables)
}
The UI of your app handles the foreground work. The user taps that button to In the background, work that might take longer is performed
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.
subscribe 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()
vm.imageView
.resizable()
.scaledToFit()
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 to OperationQueue.main
happen simultaneously.
When you see these things, you know it is time to use receive(on:).
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()
@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()
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.
}
When this view model is de-initialized and then the @Published
func createGreeting(with name: String) -> String {
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
}
}
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()
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)
}
}
The first value to come through is the empty Note: There are two types of pipelines:
init() {
string the newName property is assigned. • Error-throwing
cancellable = $newName
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
Not sure which kind of pipeline you
} else { always assigned to the same @Published
have?
nToZ.append(name) property, you could use the assign(to:)
Don’t worry, Xcode won’t let you use this
} subscriber instead.
subscriber on an error-throwing
}
pipeline.
}
func fetchRandomName() {
newName = names.randomElement()!
} Learn more in the Handling Errors
} chapter.
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
Button("Start Processing") {
Use width: 214 vm.fetch()
}
func fetch() {
Pipeline: The idea here is to check values
cancellable = [1,2,3,4,5].publisher
.tryMap { (value) -> String in coming through the pipeline and stop if
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:
print(completion) indicator and show an alert on the view.
}
data = String(data.dropLast(2))
} receiveValue: { [unowned self] (value) in
data = data.appending("\(value), ")
}
}
}
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
Text("\(vm.lastName), \(vm.firstName)")
Text("Team")
Use width: 214 .bold()
var lastNameUppercased: Just<String> { If you’re adding operators, you might find it easier to use a closure. If there’s only one item
Just("Moeykens")
.map { $0.uppercased() } in a closure then you don’t need to use the get or the return keywords.
}
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.
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.
Using eraseToAnyPublisher
By using the operator eraseToAnyPublisher, you can simplify the return type of the publishing part of the pipeline (no subscriber).
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.
AnyPublisher - View
struct AnyPublisher_Intro: View {
@StateObject private var vm = AnyPublisher_IntroViewModel()
Text("Team")
.bold()
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.
AppPublishers.teamPublisher
class AppPublishers {
.eraseToAnyPublisher()
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()
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.
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
self.artData = artData
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.
func fetch() {
let artists = ["Picasso", "Michelangelo"] The three publishers used all have varying amounts of
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.
}
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.
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
func getPercent(_ number: Double) -> String { Notice the order of the
let formatter = NumberFormatter()
results does not match
formatter.numberStyle = .percent
return formatter.string(from: NSNumber(value: number)) ?? "N/A" the order of the names
} above the button.
}
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
func fetchNameResults() { will convert the pipeline to a non-error-throwing
names.publisher pipeline and set the failure type to Never.
.map { name -> (String, URL) in
(name, URL(string: "https://api.genderize.io/?name=\(name)")!) I didn’t have to set the return type of flatMap. It will
} work just fine without it but I wanted it here so you
.flatMap { (name, url) -> AnyPublisher<NameResult, Never> in could see it and it would be more clear.
URLSession.shared.dataTaskPublisher(for: url)
.map { (data: Data, response: URLResponse) in You could throw an error from flatMap if you wanted
data to. You would just have to change the subscriber from
} sink(receiveValue:) to
.decode(type: NameResult.self, decoder: JSONDecoder()) sink(receiveCompletion:receiveValue:).
.replaceError(with: NameResult(name: name, gender: "Undetermined"))
.eraseToAnyPublisher() See more at “Handling Errors”.
}
.receive(on: RunLoop.main)
The receive operator
.sink { [unowned self] nameResult in
switches execution back to the
nameResults.append(nameResult)
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.
FlatMap - Order
class FlatMap_IntroViewModel: ObservableObject {
@Published var names = ["Kelly", "Madison", "Pat", "Alexus", "Taylor", "Tracy"]
@Published var nameResults: [NameResult] = []
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
nameResults.append(nameResult) You can’t guarantee the order in which the results are
} returned from this flatMap. All of the publishers can run
.store(in: &cancellables) all at the same time.
} You CAN control how many publishers can run at the same
} time though with the maxPublishers parameter.
See next page…
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.
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
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()
SwitchToLatest - Diagram
Taylor
Pat
struct NameResult: Decodable
{
var name = "Tracy"
Tracy Publish var gender = "female"
Madison var probability = 0.92
}
private var cancellables: Set<AnyCancellable> = [] Only one name will be sent through at a time. But many
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)
Learn More
.sink { [unowned self] nameResult in • dataTaskPublisher
self.nameResult = nameResult
} • replaceError
If the user is tapping many rows, the switchToLatest
.store(in: &cancellables)
operator will keep canceling dataTaskPublishers until one
• delay
}
} finishes and then sends the results downstream. • eraseToAnyPublisher
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()
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.
?
The zip operator will match up items from each publisher and pass colors array then “Monet”
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.
publisher publisher
.try… { … } .map { … }
.sink(receiveCompletion: { … }, .sink(receiveValue: { … })
receiveValue: { … }) // OR
.assign(to: )
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
All operators that begin with So far, the only publisher I know that can Try adding an assign(to:) subscriber. If Xcode gives
“try“ throw errors. throw an error is the dataTaskPublisher. you an error, then usually something is throwing an error.
(Decode operator)
==
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()
func fetch() {
let dataIn = ["Value 1", "Value 2", "🧨 ", "Value 3"]
_ = dataIn.publisher
.tryMap { item in
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:).
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()
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.
!
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()
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()
Button("Fetch Data") {
vm.fetch()
}
return data
}
.decode(type: [ToDo].self, decoder: JSONDecoder()) Note: The decode operator can also throw an error.
.mapError { error -> UrlResponseErrors in You can see that mapError receives an error and the
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.
The receive operator switches execution back to the
But 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
appear on the UI.
so you know where it’s coming from before changing it in some way.
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()
func fetch() {
You will not see these values published because
let dataIn = ["Value 1", "Value 2", "Value 3", "🧨 ", "Value 5", "Value 6"]
the pipeline will finish after replaceError is called.
_ = dataIn.publisher
.tryMap{ item in
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()
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()
}
}
}
func fetch() {
let url = URL(string: "https://oidutsniatnuomgib.com/")!
cancellable = URLSession.shared.dataTaskPublisher(for: url) Just because the retry is set to 2, the publisher will
actually get run 3 times.
.retry(2)
The publisher runs the first time, fails, then runs 2 more
.map { (data: Data, response: URLResponse) -> String in 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
})
}
}
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()
_ = 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
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.
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 {
VStack(spacing: 20) {
HeaderView("BreakpointOnError",
subtitle: "Introduction",
}
In this example, an error is thrown if the pipeline
.font(.title)
gets what it considers invalid data.
.onAppear {
_ = 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
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()
_ = dataIn.publisher
.handleEvents(
receiveSubscription: { subscription in
print("Event: Received subscription")
}, receiveOutput: { item in
print("Event: Received output: \(item)")
}, receiveCompletion: { completion in Note: The receiveCompletion in this
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 here:
throw InvalidPlanetError()
}
return item
}
.sink(receiveCompletion: { completion in
print("Pipeline completed")
}, receiveValue: { [unowned self] item in
dataToView.append(item)
})
}
}
Xcode Debugger Console
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()
}
}
}
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
self.data.append(datum) debug console.
}
}
}
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
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.
deinit {
print("Unloaded TestingMemory_ViewModel")
}
}
Big Mountain Studio creates premium reference materials. This means my books are more like dictionaries that show individual topics. I highly recommend you
supplement your learning with tutorial-based learning too. Included in the following pages are more Combine learning resources that this book complements. Enjoy!
More Resources
designcode.io
Good job!
MORE FROM ME
Explorers Club
GET EXCLUSIVE ACCESS TO OVER $1,000 WORTH OF BOOKS AND COURSES
SwiftUI
429
PARTNER PROGRAM
An “partner” is someone officially connected to me and Big Mountain Studio. As a partner you can sell my products with your own partner link. If someone buys a
product, you get:
20% !
If five people buy this book then you made your money back! Beyond that, you have got yourself some extra spending money. 💰