Chapters

Hide chapters

Combine: Asynchronous Programming with Swift

Third Edition · iOS 15 · Swift 5.5 · Xcode 13

4. Filtering Operators
Written by Shai Mishali

As you might have realized by now, operators are basically the vocabulary that you use to manipulate Combine publishers. The more “words” you know, the better your control of your data will be.

In the previous chapter, you learned how to consume values and transform them into other values — definitely one of the most useful operator categories for your daily work.

But what happens when you want to limit the values or events emitted by the publisher, and only consume some of them? This chapter is all about how to do this with a special group of operators: Filtering operators!

Luckily, many of these operators have parallels with the same names in the Swift standard library, so don’t be surprised if you’re able to filter some of this chapter’s content. :]

It’s time to dive right in.

Getting started

You can find the starter playground for this chapter, Starter.playground, in the projects folder. As you progress through this chapter, you’ll write code in the playground and then run the playground. This will help you understand how different operators manipulate events emitted by your publisher.

Note: Most operators in this chapter have parallels with a try prefix, for example, filter vs. tryFilter. The only difference between them is that the latter provides a throwing closure. Any error you throw from within the closure will terminate the publisher with the thrown error. For brevity’s sake, this chapter will only cover the non-throwing variations, since they are virtually identical.

Filtering basics

This first section will deal with the basics of filtering — consuming a publisher of values and conditionally deciding which of them to pass to the consumer.

The easiest way to do this is the aptly-named operator — filter, which takes a closure expectd to return a Bool. It’ll only pass down values that match the provided predicate:

filter { $0.isMultiple(of: 3) } 1 2 3 3 4 5 6 7 8 9 9 10 6

Add this new example to your playground:

example(of: "filter") {
  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .filter { $0.isMultiple(of: 3) }
    .sink(receiveValue: { n in
      print("\(n) is a multiple of 3!")
    })
    .store(in: &subscriptions)
}

In the above example, you:

  1. Create a new publisher, which will emit a finite number of values — 1 through 10, and then complete, using the publisher property on Sequence types.
  2. Use the filter operator, passing in a predicate where you only allow through numbers that are multiples of three.

Run your playground. You should see the following in your console:

——— Example of: filter ———
3 is a multiple of 3!
6 is a multiple of 3!
9 is a multiple of 3!

Such an elegant way to cheat on your math homework, isn’t it? :]

Many times in the lifetime of your app, you’ll have publishers that emit identical values in a row that you might want to ignore. For example, if a user types “a” five times in a row and then types “b”, you might want to disregard the excessive “a“s.

Combine provides the perfect operator for the task: removeDuplicates:

removeDuplicates() a b c d e a b b b c d d e f f

Notice how you don’t have to provide any arguments to this operator. removeDuplicates automatically works for any values conforming to Equatable, including String.

Add the following example of removeDuplicates() to your playground — and be sure to include a space before the ? in the words variable:

example(of: "removeDuplicates") {
  // 1
  let words = "hey hey there! want to listen to mister mister ?"
                  .components(separatedBy: " ")
                  .publisher
  // 2
  words
    .removeDuplicates()
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
}

This code isn’t too different from the last one. You:

  1. Separate a sentence into an array of words (e.g., [String]) and then create a new publisher to emit these words.
  2. Apply removeDuplicates() to your words publisher.

Run your playground and take a look at the debug console:

——— Example of: removeDuplicates ———
hey
there!
want
to
listen
to
mister
?

As you can see, you’ve skipped the second “hey” and the second “mister”. Awesome!

Note: What about values that don’t conform to Equatable? Well, removeDuplicates has another overload that takes a closure with two values, from which you’ll return a Bool to indicate whether the values are equal or not.

Compacting and ignoring

Quite often, you’ll find yourself dealing with a publisher emitting Optional values. Or even more commonly, you’ll want to perform some operation on your values that might return nil, but who wants to handle all those nils ?!

If your spidey sense is tingling, thinking of a very well-known method on Sequence from the Swift standard library called compactMap that does that job, good news – there’s also an operator with the same name!

1.24 3.0 “a” “1.24” “3” “def” “45” “0.23” 45.0 0.23 compactMap { Float($0) }

Add the following to your playground:

example(of: "compactMap") {
  // 1
  let strings = ["a", "1.24", "3",
                 "def", "45", "0.23"].publisher
  
  // 2
  strings
    .compactMap { Float($0) }
    .sink(receiveValue: {
      // 3
      print($0)
    })
    .store(in: &subscriptions)
}

Just as the diagram outlines, you:

  1. Create a publisher that emits a finite list of strings.
  2. Use compactMap to attempt to initialize a Float from each individual string. If Float’s initializer doesn’t know how to convert the provided string, it returns nil. Those nil values are automatically filtered out by the compactMap operator.
  3. Only print strings that have been successfully converted to Floats.

Run the above example in your playground and you should see output similar to the diagram above:

——— Example of: compactMap ———
1.24
3.0
45.0
0.23

All right, why don’t you take a quick break from all these values… who cares about those, right? Sometimes, all you want to know is that the publisher has finished emitting values, disregarding the actual values. When such a scenario occurs, you can use the ignoreOutput operator:

1 2 3 ... 1000 ignoreOutput()

As the diagram above shows, it doesn’t matter which values are emitted or how many of them, as they’re all ignored; you only push the completion event through to the consumer.

Experiment with this example by adding the following code to your playground:

example(of: "ignoreOutput") {
  // 1
  let numbers = (1...10_000).publisher
  
  // 2
  numbers
    .ignoreOutput()
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

In the above example, you:

  1. Create a publisher emitting 10,000 values from 1 through 10,000.
  2. Add the ignoreOutput operator, which omits all values and emits only the completion event to the consumer.

Can you guess what the output of this code will be?

If you guessed that no values will be printed, you’re right! Run your playground and check out the debug console:

——— Example of: ignoreOutput ———
Completed with: finished

Finding values

In this section, you’ll learn about two operators that also have their origins in the Swift standard library: first(where:) and last(where:). As their names imply, you use them to find and emit only the first or the last value matching the provided predicate, respectively.

Time to check out a few examples, starting with first(where:).

2 first(where: { $0 % 2 == 0 }) 1 2 3 4 5

This operator is interesting because it’s lazy, meaning: It only takes as many values as it needs until it finds one matching the predicate you provided. As soon as it finds a match, it cancels the subscription and completes.

Add the following piece of code to your playground to see how this works:

example(of: "first(where:)") {
  // 1
  let numbers = (1...9).publisher
  
  // 2
  numbers
    .first(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

Here’s what the code you’ve just added does:

  1. Creates a new publisher emitting numbers from 1 through 9.
  2. Uses the first(where:) operator to find the first emitted even value.

Run this example in your playground and look at the console output:

——— Example of: first(where:) ———
2
Completed with: finished

It works exactly like you probably guessed it would. But wait, what about the subscription to the upstream, meaning the numbers publisher? Does it keep emitting its values even after it finds a matching even number? Test this theory by finding the following line:

numbers

Then add the print("numbers") operator immediately after that line, so it looks as follows:

numbers
  .print("numbers")

Note: You can use the print operator anywhere in your operator chain to see exactly what events occur at that point.

Run your playground again, and take a look at the console. Your output should like similar to the following:

——— Example of: first(where:) ———
numbers: receive subscription: (1...9)
numbers: request unlimited
numbers: receive value: (1)
numbers: receive value: (2)
numbers: receive cancel
2
Completed with: finished

This is very interesting!

As you can see, as soon as first(where:) finds a matching value, it sends a cancellation through the subscription, causing the upstream to stop emitting values. Very handy!

Now, you can move on to the opposite of this operator — last(where:), whose purpose is to find the last value matching a provided predicate.

1 2 3 4 5 4 last(where: { $0 % 2 == 0 })

As opposed to first(where:), this operator is greedy since it must wait for the publisher to complete emitting values to know whether a matching value has been found. For that reason, the upstream must be finite.

Add the following code to your playground:

example(of: "last(where:)") {
  // 1
  let numbers = (1...9).publisher
  
  // 2
  numbers
    .last(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

Much like the previous code example, you:

  1. Create a publisher that emits numbers between 1 and 9.
  2. Use the last(where:) operator to find the last emitted even value.

Did you guess what the output will be? Run your playground and find out:

——— Example of: last(where:) ———
8
Completed with: finished

Remember I said earlier that the publisher must complete for this operator to work? Why is that?

Well, that’s because there’s no way for the operator to know if the publisher will emit a value that matches the criteria down the line, so the operator must know the full scope of the publisher before it can determine the last item matching the predicate.

To see this in action, replace the entire example with the following:

example(of: "last(where:)") {
  let numbers = PassthroughSubject<Int, Never>()
  
  numbers
    .last(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  numbers.send(1)
  numbers.send(2)
  numbers.send(3)
  numbers.send(4)
  numbers.send(5)
}

In this example, you use a PassthroughSubject and manually send events through it.

Run your playground again, and you should see… absolutely nothing:

——— Example of: last(where:) ———

As expected, since the publisher never completes, there’s no way to determine the last value matching the criteria.

To fix this, add the following as the last line of the example to send a completion through the subject:

numbers.send(completion: .finished)

Run your playground again, and everything should now work as expected:

——— Example of: last(where:) ———
4
Completed with: finished

I guess that everything must come to an end… or completion, in this case.

Note: You can also use the first() and last() operators to simply get either the first or last value ever emitted by the publisher. These are also lazy and greedy, accordingly.

Dropping values

Dropping values is a useful capability you’ll often need to leverage when working with publishers. For example, you can use it when you want to ignore values from one publisher until a second one starts publishing, or if you want to ignore a specific amount of values at the start of the stream.

Three operators fall into this category, and you’ll start by learning about the simplest one first — dropFirst.

The dropFirst operator takes a count parameter — defaulting to 1 if omitted — and ignores the first count values emitted by the publisher. Only after skipping count values, the publisher will start passing values through.

1 2 3 4 5 4 5 dropFirst(3)

Add the following code to the end of your playground to try this operator:

example(of: "dropFirst") {
  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .dropFirst(8)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
}

As in the previous diagram, you:

  1. Create a publisher that emits 10 numbers between 1 and 10.
  2. Use dropFirst(8) to drop the first eight values, printing only 9 and 10.

Run your playground and you should see the following output:

——— Example of: dropFirst ———
9
10

Simple, right? Often, the most useful operators are!

Moving on to the next operator in the value dropping family – drop(while:). This is another extremely useful variation that takes a predicate closure and ignores any values emitted by the publisher until the first time that predicate is met. As soon as the predicate is met, values begin to flow through the operator:

1 2 3 4 5 6 5 6 drop(while: { $0 % 5 != 0 })

Add the following example to your playground to see this in action:

example(of: "drop(while:)") {
  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .drop(while: { $0 % 5 != 0 })
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
}

In the following code, you:

  1. Create a publisher that emits numbers between 1 and 10.
  2. Use drop(while:) to wait for the first value that is divisible by five. As soon as the condition is met, values will start flowing through the operator and won’t be dropped anymore.

Run your playground and look at the debug console:

——— Example of: drop(while:) ———
5
6
7
8
9
10

Excellent! As you can see, you’ve dropped the first four values. As soon as 5 arrives, the question “is this divisible by five?” is finally true, so it now emits 5 and all future values.

You might ask yourself – how is this operator different from filter? Both of them take a closure that controls which values are emitted based on the result of that closure.

The first difference is that filter lets values through if you return true in the closure, while drop(while:) skips values as long you return true from the closure.

The second, and more important difference is that filter never stops evaluating its condition for all values published by the upstream publisher. Even after the condition of filter evaluates to true, further values are still “questioned” and your closure must answer the question: “Do you want to let this value through?”.

On the contrary, drop(while:)’s predicate closure will never be executed again after the condition is met. To confirm this, replace the following line:

.drop(while: { $0 % 5 != 0 })

With this piece of code:

.drop(while: {
  print("x")
  return $0 % 5 != 0
})

You added a print statement to print x to the debug console every time the closure is invoked. Run the playground and you should see the following output:

——— Example of: drop(while:) ———
x
x
x
x
x
5
6
7
8
9
10

As you might have noticed, x prints exactly five times. As soon as the condition is met (when 5 is emitted), the closure is never evaluated again.

Alrighty then. Two dropping operators down, one more to go.

The final and most elaborate operator of the filtering category is drop(untilOutputFrom:).

Imagine a scenario where you have a user tapping a button, but you want to ignore all taps until your isReady publisher emits some result. This operator is perfect for this sort of condition.

It skips any values emitted by a publisher until a second publisher starts emitting values, creating a relationship between them:

1 2 3 4 5 4 5 drop(untilOutputFrom: isReady) taps isReady

The top line represents the isReady stream and the second line represents taps by the user passing through drop(untilOutputFrom:), which takes isReady as an argument.

At the end of your playground, add the following code that reproduces this diagram:

example(of: "drop(untilOutputFrom:)") {
  // 1
  let isReady = PassthroughSubject<Void, Never>()
  let taps = PassthroughSubject<Int, Never>()
  
  // 2
  taps
    .drop(untilOutputFrom: isReady)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  // 3
  (1...5).forEach { n in
    taps.send(n)
    
    if n == 3 {
      isReady.send()
    }
  }
}

In this code, you:

  1. Create two PassthroughSubjects that you can manually send values through. The first is isReady while the second represents taps by the user.
  2. Use drop(untilOutputFrom: isReady) to ignore any taps from the user until isReady emits at least one value.
  3. Send five “taps” through the subject, just like in the diagram above. After the third tap, you send isReady a value.

Run your playground, then take a look at your debug console. You will see the following output:

——— Example of: drop(untilOutputFrom:) ———
4
5

This output is the same as the diagram above:

  • There are five taps from the user. The first three are ignored.
  • After the third tap, isReady emits a value.
  • All future taps by the user are passed through.

You’ve gained quite a mastery of getting rid of unwanted values! Now, it’s time for the final filtering operators group: Limiting values.

Limiting values

In the previous section, you’ve learned how to drop — or skip — values until a certain condition is met. That condition could be either matching some static value, a predicate closure, or a dependency on a different publisher.

This section tackles the opposite need: receiving values until some condition is met, and then forcing the publisher to complete. For example, consider a request that may emit an unknown amount of values, but you only want a single emission and don’t care about the rest of them.

Combine solves this set of problems with the prefix family of operators. Even though the name isn’t entirely intuitive, the abilities these operators provide are useful for many real-life situations.

The prefix family of operators is similar to the drop family and provides prefix(_:), prefix(while:) and prefix(untilOutputFrom:). However, instead of dropping values until some condition is met, the prefix operators take values until that condition is met.

Now, it’s time for you to dive into the final set of operators for this chapter, starting with prefix(_:).

As the opposite of dropFirst, prefix(_:) will take values only up to the provided amount and then complete:

2 1 prefix(2) 1 2 3 4

Add the following code to your playground to demonstrate this:

example(of: "prefix") {
  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .prefix(2)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

This code is quite similar to the drop code you used in the previous section. You:

  1. Create a publisher that emits numbers from 1 through 10.
  2. Use prefix(2) to allow the emission of only the first two values. As soon as two values are emitted, the publisher completes.

Run your playground and you’ll see the following output:

——— Example of: prefix ———
1
2
Completed with: finished

Just like first(where:), this operator is lazy, meaning it only takes up as many values as it needs and then terminates. This also prevents numbers from producing additional values beyond 1 and 2, since it also completes.

Next up is prefix(while:), which takes a predicate closure and lets values from the upstream publisher through as long as the result of that closure is true. As soon as the result is false, the publisher will complete:

1 2 2 1 prefix(while: { $0 < 3 }) 3 4 5

Add the following example to your playground to try this:

example(of: "prefix(while:)") {
  // 1
  let numbers = (1...10).publisher
  
  // 2
  numbers
    .prefix(while: { $0 < 3 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
}

This example is mostly identical to the previous one, aside from using a closure to evaluate the prefixing condition. You:

  1. Create a publisher that emits values between 1 and 10.
  2. Use prefix(while:) to let values through as long as they’re smaller than 3. As soon as a value equal to or larger than 3 is emitted, the publisher completes.

Run the playground and check out the debug console; the output should be identical to the one from the previous operator:

——— Example of: prefix(while:) ———
1
2
Completed with: finished

With the first two prefix operators behind us, it’s time for the most complex one: prefix(untilOutputFrom:). Once again, as opposed to drop(untilOutputFrom:) which skips values until a second publisher emits, prefix(untilOutputFrom:) takes values until a second publisher emits.

Imagine a scenario where you have a button that the user can only tap twice. As soon as two taps occur, further tap events on the button should be omitted:

1 2 1 2 prefix(untilOutputFrom: isReady) taps isReady 3 4 5

Add the final example for this chapter to the end of your playground:

example(of: "prefix(untilOutputFrom:)") {
  // 1
  let isReady = PassthroughSubject<Void, Never>()
  let taps = PassthroughSubject<Int, Never>()
  
  // 2
  taps
    .prefix(untilOutputFrom: isReady)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
  
  // 3
  (1...5).forEach { n in
    taps.send(n)
    
    if n == 2 {
      isReady.send()
    }
  }
}

If you think back to the drop(untilOutputFrom:) example, you should find this easy to understand. You:

  1. Create two PassthroughSubjects that you can manually send values through. The first is isReady while the second represents taps by the user.
  2. Use prefix(untilOutputFrom: isReady) to let tap events through until isReady emits at least one value.
  3. Send five “taps” through the subject, exactly as in the diagram above. After the second tap, you send isReady a value.

Run the playground. Looking at the console, you should see the following:

——— Example of: prefix(untilOutputFrom:) ———
1
2
Completed with: finished

Challenge

You have quite a lot of filtering knowledge at your disposal now. Why not try a short challenge?

Challenge: Filter all the things

Create an example that publishes a collection of numbers from 1 through 100, and use filtering operators to:

  1. Skip the first 50 values emitted by the upstream publisher.
  2. Take the next 20 values after those first 50 values.
  3. Only take even numbers.

The output of your example should produce the following numbers, one per line:

52 54 56 58 60 62 64 66 68 70

Note: In this challenge, you’ll need to chain multiple operators together to produce the desired values.

You can find the full solution to this challenge in projects/challenge/Final.playground.

Key points

In this chapter, you learned that:

  • Filtering operators let you control which values emitted by the upstream publisher are sent downstream, to another operator or to the consumer.

  • When you don’t care about the values themselves, and only want a completion event, ignoreOutput is your friend.

  • Finding values is another sort of filtering, where you can find the first or last values to match a provided predicate using first(where:) and last(where:), respectively.

  • First-style operators are lazy; they take only as many values as needed and then complete. Last-style operators are greedy and must know the full scope of the values before deciding which of the values is the last to fulfill the condition.

  • You can control how many values emitted by the upstream publisher are ignored before sending values downstream by using the drop family of operators.

  • Similarly, you can control how many values the upstream publisher may emit before completing by using the prefix family of operators.

Where to go from here?

Wow, what a ride this chapter has been! You should rightfully feel like a master of filtering, ready to channel these upstream values in any way you desire.

With the knowledge of transforming and filtering operators already in your tool belt, it’s time for you to move to the next chapter and learn another extremely useful group of operators: Combining operators.

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.