Leave a rating/review
Now with the basics of filtering out of the way, let’s focus in on some operators that will help you refine the values you’ll send downstream to consumers.
Let’s talk about a few operators that have parallels to functions in the Swift Standard Library. You’ll begin with first(where:) which, as the marble diagram shows, returns the first value from the publisher that causes the closure passed into the operator to return true.
This operator is lazy - it only takes as many values as it needs to find one that matches the predicate. Once that happens, it cancels the subscription and completes. Let’s take a look at that in a demo.
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 you create a publisher from the range of numbers from 1 through 9, and then attach a first(where) operator that looks for the first instance of an even value. Run the playground and you’ll see that, as expected it returns 2. Let’s do a quick check to
see if the publisher keeps emitting values. Add the print operator before that call to first, to see what the pipeline is doing.
numbers
.print("numbers")
Running the playground you see that indeed, the publisher gets a cancellation after emitting the value 2.
On the flip side of first(where:) is last(where:), which is greedy - it needs all the values to know whether a matching value has been found (because if it doesn’t, how will it know if the last one it found is, well, the last?). Therefore this requires a publisher that will complete at some point. Let’s take a look.
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)
}
This looks very familiar to the example for first(where:), returning 8 as the result. You can even send values one by one. Paste this code in the playground and run it.
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)
numbers.send(completion: .finished)
}
Notice how 4 is correctly detected as the last even number sent before the completion.
In some cases you want to receive values until some condition is met, and then force the publisher to complete. The prefix family of operators is a good fit for this. One is aptly named - prefix. This operator only takes up to the specified number of values, and then completes. Let’s take a look.
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)
}
Here you create the usual publisher from a sequence, and then applying the prefix operator, the playground returns 1 and 2 as accepted values. Like first(where:) this operator is lazy, only taking what it needs, and then completing.
On the flip side of prefix is the family of drop operators. These can ignore values up to some condition, and then start accepting them. The marble diagram for drop(while:) shows that it ignores the first 4 values, and then passes the remaining values to the consumer. This is very much the opposite of prefix, which took the first n values and ignored the rest.
(By the way, prefix has a corresponding prefix(while:) variant that works in a similar fashion.) The predicate passed into the operator is used to determine how many values to drop. Let’s take a look.
example(of: "drop(while:)") {
// 1
let numbers = (1...10).publisher
// 2
numbers
.drop(while: { $0 % 5 != 0 })
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
Here, after setting up the standard publisher example, you use the drop operator to filter out values that aren’t divisible by 5 and lower than 5.
In this part of the course, you learned all about Filtering Operators. Filtering Operators let you control which values emitted by publishers get sent downstream. ignoreOutput can be used to skip values entirely, only receiving the completion event. Finding values can be performed with first(where:), which is lazy, and last(where:), which is greedy.
The drop family of operators can be used to ignore values in the stream before sending the rest downstream, and the prefix family can be used to send a certain number of values downstream before completing.
Next, it’s a time a for a challenge with all of these filtering operators. Join me in the next episode.