Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

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:

1 -50 246 0 -50 min()

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:

  1. Create a publisher emitting four different numbers.
  2. 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:

  1. You create a publisher that emits three Data objects created from various strings.
  2. Since Data doesn’t conform to Comparable, you use the min(by:) operator to find the Data object with the smallest number of bytes.
  3. 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:

“A” “F” “Z” “E” “Z” max()

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:

  1. Create a publisher that emits four different letters.
  2. 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 companion max(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.

“A” first() “A” “B” “С”

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:

  1. Create a publisher emitting three letters.
  2. 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:

  1. Create a publisher that emits four letters.
  2. Use the first(where:) operator to find the first letter contained in Hello 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:

“A” “B” “C” “C” last()

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:

  1. Create a publisher that will emit three letters and finish.
  2. 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 a last(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:

“B” output(at: 1) “A” “B” “С”

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:

  1. Create a publisher which emits three letters.
  2. Use output(at:) to only let through the value emitted at index 1 — 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:

“D” “C” “B” output(in: 1...3) “A” “B” “С” “D” “E”

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:

  1. Create a publisher that emits five different letters.
  2. Use the output(in:) operator to only let through values emitted in indices 1 through 3, 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:

3 count() “A” “B” “С”

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:

  1. Create a publisher that emits three letters.
  2. 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:

true contains(“C”) “A” “B” “С” “E” “D”

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:

  1. Create a publisher emitting five different letters — A through E — and create a letter value to use with contains.
  2. Use contains to check if the upstream publisher emitted the value of letter: C.
  3. 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:

  1. Define a Person struct with an id and a name.
  2. Create a publisher that emits three different instances of People.
  3. Use contains to see if the id of any of them is 800.
  4. 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:

true 0 2 4 allSatisfy { $0 % 2 == 0 }

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:

  1. Create a publisher that emits numbers between 0 to 5 in steps of 2 (i.e., 0, 2 and 4).
  2. 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:

11 1 3 7 reduce(0) { acc, value in acc + value }

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:

  1. Create a publisher that emits six Strings.
  2. 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 and reduce have the same functionality, with the main difference being that scan emits the accumulated value for every emitted value, while reduce emits a single accumulated value once the upstream publisher sends a .finished completion event. Feel free to change reduce to scan 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 and max to emit the minimum or maximum value emitted by a publisher, respectively.
  • first, last and output(at:) are useful when you want to find a value emitted at a specific index. Use output(in:) to find values emitted within a range of indices.
  • first(where:) and last(where:) each take a predicate to determine which values it should let through.
  • Operators such as count, contains and allSatisfy 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.