7.
Sequence Operators
Written by Shai Mishali
At this point, you know most of the operators that Combine has to offer! How great is that? There’s still one more category for you to dig into, though: Sequence Operators.
Sequence operators are easiest to understand when you realize that publishers are just sequences themselves. Sequence operators work with a publisher’s values, much like an array or a set — which, of course, are just finite sequences!
With that in mind, sequence operators mostly deal with the publisher as a whole and not with individual values, as other operator categories do.
Many of the operators in this category have nearly identical names and behaviors as their counterparts in the Swift standard library.
Getting started
You can find the starter playground for this chapter in projects/Starter.playground. Throughout this chapter, you’ll add code to your playground and run it to see how these different sequence operators manipulate your publisher. You’ll use the print
operator to log all publishing events.
Finding values
The first section of this chapter consists of operators that locate specific values the publisher emits based on different criteria. These are similar to the collection methods in the Swift standard library.
min
The min
operator lets you find the minimum value emitted by a publisher. It’s greedy, which means it must wait for the publisher to send a .finished
completion event. Once the publisher completes, only the minimum value is emitted by the operator:
Add the following example to your playground to try min
:
example(of: "min") {
// 1
let publisher = [1, -50, 246, 0].publisher
// 2
publisher
.print("publisher")
.min()
.sink(receiveValue: { print("Lowest value is \($0)") })
.store(in: &subscriptions)
}
In this code, you:
- Create a publisher emitting four different numbers.
- Use the
min
operator to find the minimum number emitted by the publisher and print that value.
Run your playground and you’ll see the following output in the console:
——— Example of: min ———
publisher: receive subscription: ([1, -50, 246, 0])
publisher: request unlimited
publisher: receive value: (1)
publisher: receive value: (-50)
publisher: receive value: (246)
publisher: receive value: (0)
publisher: receive finished
Lowest value is -50
As you can see, the publisher emits all its values and finishes, then min
finds the minimum and sends it downstream to sink
to print it out.
But wait, how does Combine know which of these numbers is the minimum? Well, that’s thanks to the fact numeric values conform to the Comparable
protocol. You can use min()
directly, without any arguments, on publishers that emit Comparable
-conforming types.
But what happens if your values don’t conform to Comparable
? Luckily, you can provide your own comparator closure using the min(by:)
operator.
Consider the following example, where your publisher emits many pieces of Data
and you’d like to find the smallest one.
Add the following code to your playground:
example(of: "min non-Comparable") {
// 1
let publisher = ["12345",
"ab",
"hello world"]
.map { Data($0.utf8) } // [Data]
.publisher // Publisher<Data, Never>
// 2
publisher
.print("publisher")
.min(by: { $0.count < $1.count })
.sink(receiveValue: { data in
// 3
let string = String(data: data, encoding: .utf8)!
print("Smallest data is \(string), \(data.count) bytes")
})
.store(in: &subscriptions)
}
In the above code:
- You create a publisher that emits three
Data
objects created from various strings. - Since
Data
doesn’t conform toComparable
, you use themin(by:)
operator to find theData
object with the smallest number of bytes. - You convert the smallest
Data
object back to a string and print it out.
Run your playground and you’ll see the following in your console:
——— Example of: min non-Comparable ———
publisher: receive subscription: ([5 bytes, 2 bytes, 11 bytes])
publisher: request unlimited
publisher: receive value: (5 bytes)
publisher: receive value: (2 bytes)
publisher: receive value: (11 bytes)
publisher: receive finished
Smallest data is ab, 2 bytes
Like the previous example, the publisher emits all its Data
objects and finishes, then min(by:)
finds and emits the data with the smallest byte size and sink
prints it out.
max
As you’d guess, max
works exactly like min
, except that it finds the maximum value emitted by a publisher:
Add the following code to your playground to try this example:
example(of: "max") {
// 1
let publisher = ["A", "F", "Z", "E"].publisher
// 2
publisher
.print("publisher")
.max()
.sink(receiveValue: { print("Highest value is \($0)") })
.store(in: &subscriptions)
}
In the following code, you:
- Create a publisher that emits four different letters.
- Use the
max
operator to find the letter with the highest value and print it.
Run your playground. You’ll see the following output in your playground:
——— Example of: max ———
publisher: receive subscription: (["A", "F", "Z", "E"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive value: (F)
publisher: receive value: (Z)
publisher: receive value: (E)
publisher: receive finished
Highest value is Z
Exactly like min
, max
is greedy and must wait for the upstream publisher to finish emitting its values before it determines the maximum value. In this case, that value is Z
.
Note: Exactly like
min
,max
also has a companionmax(by:)
operator that accepts a predicate to determine the maximum value emitted among non-Comparable
values.
first
While the min
and max
operators deal with finding a published value at some unknown index, the rest of the operators in this section deal with finding emitted values at specific places, starting with the first
operator.
The first
operator is similar to Swift’s first
property on collections, except that it lets the first emitted value through and then completes. It’s lazy, meaning it doesn’t wait for the upstream publisher to finish, but instead will cancel the subscription when it receives the first value emitted.
Add the above example to your playground:
example(of: "first") {
// 1
let publisher = ["A", "B", "C"].publisher
// 2
publisher
.print("publisher")
.first()
.sink(receiveValue: { print("First value is \($0)") })
.store(in: &subscriptions)
}
In the above code, you:
- Create a publisher emitting three letters.
- Use
first()
to let only the first emitted value through and print it out.
Run your playground and take a look at the console:
——— Example of: first ———
publisher: receive subscription: (["A", "B", "C"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive cancel
First value is A
As soon as first()
gets the publisher’s first value, it cancels the subscription to the upstream publisher.
If you’re looking for more granular control, you can also use first(where:)
. Just like its counterpart in the Swift standard library, it will emit the first value that matches a provided predicate — if there is one.
Add the following example to your playground:
example(of: "first(where:)") {
// 1
let publisher = ["J", "O", "H", "N"].publisher
// 2
publisher
.print("publisher")
.first(where: { "Hello World".contains($0) })
.sink(receiveValue: { print("First match is \($0)") })
.store(in: &subscriptions)
}
In this code, you:
- Create a publisher that emits four letters.
- Use the
first(where:)
operator to find the first letter contained inHello World
and then print it out.
Run the playground and you’ll see the following output:
——— Example of: first(where:) ———
publisher: receive subscription: (["J", "O", "H", "N"])
publisher: request unlimited
publisher: receive value: (J)
publisher: receive value: (O)
publisher: receive value: (H)
publisher: receive cancel
First match is H
In the above example, the operator checks if Hello World
contains the emitted letter until it finds the first match: H
. Upon finding that much, it cancels the subscription and emits the letter for sink
to print out.
last
Just as min
has an opposite, max
, first
also has an opposite: last
!
last
works exactly like first
, except it emits the last value that the publisher emits. This means it’s also greedy and must wait for the upstream publisher to finish:
Add this example to your playground:
example(of: "last") {
// 1
let publisher = ["A", "B", "C"].publisher
// 2
publisher
.print("publisher")
.last()
.sink(receiveValue: { print("Last value is \($0)") })
.store(in: &subscriptions)
}
In this code, you:
- Create a publisher that will emit three letters and finish.
- Use the
last
operator to only emit the last value published and print it out.
Run the playground and you’ll see the following output:
——— Example of: last ———
publisher: receive subscription: (["A", "B", "C"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive value: (B)
publisher: receive value: (C)
publisher: receive finished
Last value is C
last
waits for the upstream publisher to send a .finished
completion event, at which point it sends the last emitted value downstream to be printed out in sink
.
Note: Exactly like
first
,last
also has alast(where:)
overload, which emits the last value emitted by a publisher that matches a specified predicate.
output(at:)
The last two operators in this section don’t have counterparts in the Swift standard library. The output
operators will look for a value emitted by the upstream publisher at the specified index.
You’ll start with output(at:)
, which emits only the value emitted at the specified index:
Add the following code to your playground to try this example:
example(of: "output(at:)") {
// 1
let publisher = ["A", "B", "C"].publisher
// 2
publisher
.print("publisher")
.output(at: 1)
.sink(receiveValue: { print("Value at index 1 is \($0)") })
.store(in: &subscriptions)
}
In the above code, you:
- Create a publisher which emits three letters.
- Use
output(at:)
to only let through the value emitted at index1
— i.e., the second value.
Run the example in your playground and peek at your console:
——— Example of: output(at:) ———
publisher: receive subscription: (["A", "B", "C"])
publisher: request unlimited
publisher: receive value: (A)
publisher: request max: (1) (synchronous)
publisher: receive value: (B)
Value at index 1 is B
publisher: receive cancel
Here, the output indicates the value at index 1
is B
. However, you might’ve noticed an additional interesting fact: The operator demands one more value after every emitted value, since it knows it’s only looking for a single item. While this is an implementation detail of the specific operator, it provides interesting insight into how Apple designs some of their own built-in Combine operators to leverage backpressure.
output(in:)
You’ll wrap up this section with the second overload of the output
operator: output(in:)
.
While output(at:)
emits a single value emitted at a specified index, output(in:)
emits values whose indices are within a provided range:
To try this out, add the following example to your playground:
example(of: "output(in:)") {
// 1
let publisher = ["A", "B", "C", "D", "E"].publisher
// 2
publisher
.output(in: 1...3)
.sink(receiveCompletion: { print($0) },
receiveValue: { print("Value in range: \($0)") })
.store(in: &subscriptions)
}
In the previous code, you:
- Create a publisher that emits five different letters.
- Use the
output(in:)
operator to only let through values emitted in indices1
through3
, then print out those values.
Can you guess what the output of this example will be? Run your playground and find out:
——— Example of: output(in:) ———
Value in range: B
Value in range: C
Value in range: D
finished
Well, did you guess correctly? The operator emits individual values within the range of indices, not a collection of them. The operator prints the values B
, C
and D
as they’re in indices 1
, 2
and 3
, respectively. Then, since all items within the range have been emitted, it cancels the subscription as soon as it receives all values within the provided range.
Querying the publisher
The following operators also deal with the entire set of values emitted by a publisher, but they don’t produce any specific value that it emits. Instead, these operators emit a different value representing some query on the publisher as a whole. A good example of this is the count
operator.
count
The count
operator will emit a single value - the number of values were emitted by the upstream publisher, once the publisher sends a .finished
completion event:
Add the following code to try this example:
example(of: "count") {
// 1
let publisher = ["A", "B", "C"].publisher
// 2
publisher
.print("publisher")
.count()
.sink(receiveValue: { print("I have \($0) items") })
.store(in: &subscriptions)
}
In the above code, you:
- Create a publisher that emits three letters.
- Use
count()
to emit a single value indicating the number of values emitted by the upstream publisher.
Run your playground and check your console. You’ll see the following output:
——— Example of: count ———
publisher: receive subscription: (["A", "B", "C"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive value: (B)
publisher: receive value: (C)
publisher: receive finished
I have 3 items
As expected, the value 3
is only printed out once the upstream publisher sends a .finished
completion event.
contains
Another useful operator is contains
. You’ve probably used its counterpart in the Swift standard library more than once.
The contains
operator will emit true
and cancel the subscription if the specified value is emitted by the upstream publisher, or false
if none of the emitted values are equal to the specified one:
Add the following to your playground to try contains
:
example(of: "contains") {
// 1
let publisher = ["A", "B", "C", "D", "E"].publisher
let letter = "C"
// 2
publisher
.print("publisher")
.contains(letter)
.sink(receiveValue: { contains in
// 3
print(contains ? "Publisher emitted \(letter)!"
: "Publisher never emitted \(letter)!")
})
.store(in: &subscriptions)
}
In the previous code, you:
- Create a publisher emitting five different letters —
A
throughE
— and create aletter
value to use withcontains
. - Use
contains
to check if the upstream publisher emitted the value ofletter
:C
. - Print an appropriate message based on whether or not the value was emitted.
Run your playground and check the console:
——— Example of: contains ———
publisher: receive subscription: (["A", "B", "C", "D", "E"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive value: (B)
publisher: receive value: (C)
publisher: receive cancel
Publisher emitted C!
Huzzah! You got a message indicating C
was emitted by the publisher. You might have also noticed contains
is lazy, as it only consumes as many upstream values as it needs to perform its work. Once C
is found, it cancels the subscription and doesn’t produce any further values.
Why don’t you try another variation? Replace the following line:
let letter = "C"
With:
let letter = "F"
Next, run your playground again. You’ll see the following output:
——— Example of: contains ———
publisher: receive subscription: (["A", "B", "C", "D", "E"])
publisher: request unlimited
publisher: receive value: (A)
publisher: receive value: (B)
publisher: receive value: (C)
publisher: receive value: (D)
publisher: receive value: (E)
publisher: receive finished
Publisher never emitted F!
In this case, contains
waits for the publisher to emit F
. However, the publisher finishes without emitting F
, so contains
emits false
and you see the appropriate message printed out.
Finally, sometimes you want to look for a match for a predicate that you provide or check for the existence of an emitted value that doesn’t conform to Comparable
. For these specific cases, you have contains(where:)
.
Add the following example to your playground:
example(of: "contains(where:)") {
// 1
struct Person {
let id: Int
let name: String
}
// 2
let people = [
(123, "Shai Mishali"),
(777, "Marin Todorov"),
(214, "Florent Pillet")
]
.map(Person.init)
.publisher
// 3
people
.contains(where: { $0.id == 800 })
.sink(receiveValue: { contains in
// 4
print(contains ? "Criteria matches!"
: "Couldn't find a match for the criteria")
})
.store(in: &subscriptions)
}
The previous code is a bit more complex, but not by much. You:
- Define a
Person
struct with anid
and aname
. - Create a publisher that emits three different instances of
People
. - Use
contains
to see if theid
of any of them is800
. - Print an appropriate message based on the emitted result.
Run your playground and you’ll see the following output:
——— Example of: contains(where:) ———
Couldn't find a match for the criteria
It didn’t find any matches, as expected, because none of the emitted people have an id
of 800
.
Next, change the implementation of contains(where:)
:
.contains(where: { $0.id == 800 })
To the following:
.contains(where: { $0.id == 800 || $0.name == "Marin Todorov" })
Run the playground again and look at the console:
——— Example of: contains(where:) ———
Criteria matches!
This time it found a value matching the predicate, since Marin is indeed one of the people in your list. Awesome! :]
allSatisfy
A bunch of operators down, and only two to go! Both of them have counterpart collection methods in the Swift standard library.
You’ll start with allSatisfy
, which takes a closure predicate and emits a Boolean indicating whether all values emitted by the upstream publisher match that predicate. It’s greedy and will, therefore, wait until the upstream publisher emits a .finished
completion event:
Add the following example to your playground to try this:
example(of: "allSatisfy") {
// 1
let publisher = stride(from: 0, to: 5, by: 2).publisher
// 2
publisher
.print("publisher")
.allSatisfy { $0 % 2 == 0 }
.sink(receiveValue: { allEven in
print(allEven ? "All numbers are even"
: "Something is odd...")
})
.store(in: &subscriptions)
}
In the above code, you:
- Create a publisher that emits numbers between
0
to5
in steps of2
(i.e.,0
,2
and4
). - Use
allSatisfy
to check if all emitted values are even, then print an appropriate message based on the emitted result.
Run the code and check the console output:
——— Example of: allSatisfy ———
publisher: receive subscription: (Sequence)
publisher: request unlimited
publisher: receive value: (0)
publisher: receive value: (2)
publisher: receive value: (4)
publisher: receive finished
All numbers are even
Since all values are indeed even, the operator emits true
after the upstream publisher sends a .finished
completion, and the appropriate message is printed out.
However, if even a single value doesn’t pass the predicate condition, the operator will emit false
immediately and will cancel the subscription.
Replace the following line:
let publisher = stride(from: 0, to: 5, by: 2).publisher
With:
let publisher = stride(from: 0, to: 5, by: 1).publisher
You simply changed the stride
to step between 0
and 5
by 1
, instead of 2
. Run the playground once again and take a look at the console:
——— Example of: allSatisfy ———
publisher: receive subscription: (Sequence)
publisher: request unlimited
publisher: receive value: (0)
publisher: receive value: (1)
publisher: receive cancel
Something is odd...
In this case, as soon as 1
is emitted, the predicate doesn’t pass anymore, so allSatisfy
emits false
and cancels the subscription.
reduce
Well, here we are! The final operator for this rather packed chapter: reduce
.
The reduce
operator is a bit different from the rest of the operators covered in this chapter. It doesn’t look for a specific value or query the publisher as a whole. Instead, it lets you iteratively accumulate a new value based on the emissions of the upstream publisher.
This might sound confusing at first, but you’ll get it in a moment. The easiest way to start is with a diagram:
Combine’s reduce
operator works like its counterparts in the Swift standard library: reduce(_:_)
and reduce(into:_:)
.
It lets you provide a seed value and an accumulator closure. That closure receives the accumulated value — starting with the seed value — and the current value. From that closure, you return a new accumulated value. Once the operator receives a .finished
completion event, it emits the final accumulated value.
In the case of the above diagram, you can think of it this way :
Seed value is 0
Receives 1, 0 + 1 = 1
Receives 3, 1 + 3 = 4
Receives 7, 4 + 7 = 11
Emits 11
Time for you to try a quick example to get a better sense of this operator. Add the following to your playground:
example(of: "reduce") {
// 1
let publisher = ["Hel", "lo", " ", "Wor", "ld", "!"].publisher
publisher
.print("publisher")
.reduce("") { accumulator, value in
// 2
accumulator + value
}
.sink(receiveValue: { print("Reduced into: \($0)") })
.store(in: &subscriptions)
}
In this code, you:
- Create a publisher that emits six
String
s. - Use
reduce
with a seed of an empty string, appending the emitted values to it to create the final string result.
Run the playground and take a look at the console output:
——— Example of: reduce ———
publisher: receive subscription: (["Hel", "lo", " ", "Wor", "ld", "!"])
publisher: request unlimited
publisher: receive value: (Hel)
publisher: receive value: (lo)
publisher: receive value: ( )
publisher: receive value: (Wor)
publisher: receive value: (ld)
publisher: receive value: (!)
publisher: receive finished
Reduced into: Hello World!
Notice how the accumulated result — Hello World!
— is only printed once the upstream publisher sent a .finished
completion event.
The second argument for reduce
is a closure that takes two values of some type and returns a value of that same type. In Swift, +
is an also a function that matches that signature.
So as a final neat trick, you can reduce the syntax above. Replace the following code:
.reduce("") { accumulator, value in
// 3
return accumulator + value
}
With simply:
.reduce("", +)
If you run your playground again, it will work exactly the same as before, with a bit of a fancier syntax. ;]
Note: Does this operator feel a bit familiar? Well, that might be because you learned about
scan
in Chapter 3, “Transforming Operators.”scan
andreduce
have the same functionality, with the main difference being thatscan
emits the accumulated value for every emitted value, whilereduce
emits a single accumulated value once the upstream publisher sends a.finished
completion event. Feel free to changereduce
toscan
in the above example and try it out for yourself.
Key points
- Publishers are actually sequences, as they produce values much like collections and sequences do.
- You can use
min
andmax
to emit the minimum or maximum value emitted by a publisher, respectively. -
first
,last
andoutput(at:)
are useful when you want to find a value emitted at a specific index. Useoutput(in:)
to find values emitted within a range of indices. -
first(where:)
andlast(where:)
each take a predicate to determine which values it should let through. - Operators such as
count
,contains
andallSatisfy
don’t emit values emitted by the publisher. Rather, they emit a different value based on the emitted values. -
contains(where:)
takes a predicate to determine if the publisher contains the given value. - Use
reduce
to accumulate emitted values into a single value.
Where to go from here?
Congrats on completing the last chapter on operators for this book! give yourself a quick pat on the back and high-five yourself while you’re at it. :]
You’ll wrap up this section by working on your first practical project, where you’ll build a Collage app using Combine and many of the operators you’ve learned. Take a few deep breaths, grab a cup of coffee, and move on to the next chapter.