5.
Filtering Operators
Written by Scott Gardner
Learning a new technology stack is a bit like building a skyscraper. You’ve got to build a solid foundation before you can reach the sky. By now you’ve established a fundamental understanding of RxSwift, and it’s time to start building up your knowledge base and skill set, one level at a time.
This chapter will teach you about RxSwift’s filtering operators you can use to apply conditional constraints to emitted events, so that the subscriber only receives the elements it wants to deal with. If you’ve ever used the filter(_:) method in the Swift standard library, you’re already half way there. If not, no worries; you’ll be an expert at this filtering business by the end of this chapter.
Getting started
The starter project for this chapter is named RxPlayground. After running ./bootstrap.sh in the project folder, Xcode will open. Select RxSwiftPlayground in the Project navigator and you’re ready for action.
Ignoring operators
You’re going to jump right in and look at some useful filtering operators in RxSwift, beginning with ignoreElements. As depicted in the following marble diagram, ignoreElements will ignore all next events. It will, however, allow stop events through, such as completed or error events.
Allowing stop events through is usually implied in all marble diagrams. It’s explicitly called out this time because that’s all ignoreElements will let through.
Note: Up to now you’ve seen marble diagrams used for types. This form of marble diagram helps you visualize how operators work. The top line is the observable that is being subscribed to. The box represents the operator and its parameters, and the bottom line is the subscriber, or more specifically, what the subscriber will receive after the operator does its work.
To see ignoreElements in action, add this example to your playground:
example(of: "ignoreElements") {
// 1
let strikes = PublishSubject<String>()
let disposeBag = DisposeBag()
// 2
strikes
.ignoreElements()
.subscribe { _ in
print("You're out!")
}
.disposed(by: disposeBag)
}
Here’s what you did:
- Create a
strikessubject. - Subscribe to all
strikes’ events, but ignore allnextevents by usingignoreElements.
Note: If you don’t happen to know much about strikes, batters, and the game of baseball in general, you can read up on that when you decide to take a little break from programming: https://simple.wikipedia.org/wiki/Baseball.
The ignoreElements operator is useful when you only want to be notified when an observable has terminated, via a completed or error event. Add this code to the example:
strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")
Even though this batter can’t seem to hit the broad side of a barn and has clearly struck out, nothing is printed, because you’re ignoring all next events. It’s up to you to add a completed event to this subject in order to let the subscriber be notified. Add this code to do that:
strikes.onCompleted()
Now the subscriber will receive the completed event, and print that catchphrase no batter ever wants to hear:
--- Example of: ignoreElements ---
You're out!
The investigative reader might notice that ignoreElements actually returns a Completable, which makes sense because it will only emit a completed or error event.
There may be times when you only want to handle the nth (ordinal) element emitted by an observable, such as the third strike. For that, you can use elementAt, which takes the index of the element you want to receive, and ignores everything else.
In the marble diagram, elementAt is passed an index of 1, so it only lets through the second element.
Add this new example:
example(of: "elementAt") {
// 1
let strikes = PublishSubject<String>()
let disposeBag = DisposeBag()
// 2
strikes
.elementAt(2)
.subscribe(onNext: { _ in
print("You're out!")
})
.disposed(by: disposeBag)
}
Here’s the play-by-play:
- You create a subject.
- You subscribe to the
nextevents, ignoring all but the 3rdnextevent, found at index2.
Now you can simply add new strikes onto the subject, and your subscription will take care of letting you know when the batter has struck out. Add this code:
strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")
“Hey batta, batta, batta — swing batta!”
--- Example of: elementAt ---
You're out!
An interesting fact about element(at:): As soon as an element is emitted at the provided index, the subscription is terminated.
ignoreElements and elementAt are filtering elements emitted by an observable. When your filtering needs go beyond all or one, use the filter operator. It takes a predicate closure and applies it to every element emitted, allowing through only those elements for which the predicate resolves to true.
Check out this marble diagram, where only 1 and 2 are let through because the filter’s predicate only allows elements that are less than 3.
Add this example to your playground:
example(of: "filter") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5, 6)
// 2
.filter { $0.isMultiple(of: 2) }
// 3
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
From the top:
- You create an observable of some predefined integers.
- You use the
filteroperator to apply a conditional constraint to prevent odd numbers from getting through. - You subscribe and print out the elements that pass the filter predicate.
The result of applying this filter is that only even numbers are printed:
--- Example of: filter ---
2
4
6
Skipping operators
When you want to skip a certain number of elements, use the skip operator. It lets you ignore the first n elements, where n is the number you pass as its parameter. This marble diagram shows skip is passed 2, so it ignores the first 2 elements.
Add this new example to your playground:
example(of: "skip") {
let disposeBag = DisposeBag()
// 1
Observable.of("A", "B", "C", "D", "E", "F")
// 2
.skip(3)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an observable of letters.
- Use
skipto skip the first3elements and subscribe tonextevents.
After skipping the first 3 elements, only D, E, and F are printed:
--- Example of: skip ---
D
E
F
There’s a small family of skip operators. Like filter, skipWhile lets you include a predicate to determine what is skipped. However, unlike filter, which filters elements for the life of the subscription, skipWhile only skips up until something is not skipped, and then it lets everything else through from that point on.
And with skipWhile, returning true will cause the element to be skipped, and returning false will let it through. It’s the opposite of filter.
In this marble diagram, 1 is prevented because 1 % 2 equals 1, but then 2 is allowed through because it fails the predicate, and 3 (and everything else going forward) gets through because skipWhile is no longer skipping.
Add this new example to your playground:
example(of: "skipWhile") {
let disposeBag = DisposeBag()
// 1
Observable.of(2, 2, 3, 4, 4)
// 2
.skipWhile { $0.isMultiple(of: 2) }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
Here’s what you did:
- Create an observable of integers.
- Use
skipWhilewith a predicate that skips elements until an odd integer is emitted.
Remember, skip only skips elements up until the first element is let through, and then all remaining elements are allowed through. So this example prints:
--- Example of: skipWhile ---
3
4
4
For example, if you are developing an insurance claims app, you could use skipWhile to deny coverage until the deductible is met.
So far, you’ve filtered based on a static condition. What if you wanted to dynamically filter elements based on another observable? There are a couple of operators to choose from.
The first is skipUntil, which will keep skipping elements from the source observable — the one you’re subscribing to — until some other trigger observable emits. In this marble diagram, skipUntil ignores elements emitted by the source observable on the top line until the trigger observable on second line emits a next event. Then it stops skipping and lets everything through from that point on.
Add this example to see how skipUntil works in code:
example(of: "skipUntil") {
let disposeBag = DisposeBag()
// 1
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
// 2
subject
.skipUntil(trigger)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
In this code, you:
- Create a subject to model the data you want to work with, and another subject to act as a trigger.
- Use
skipUntiland pass thetriggersubject. Whentriggeremits,skipUntilstops skipping.
Add a couple of next events onto subject:
subject.onNext("A")
subject.onNext("B")
Nothing is printed, because you’re skipping. Now add a new next event onto trigger:
trigger.onNext("X")
This causes skipUntil to stop skipping. From this point onward, all elements are let through. Add another next event onto subject:
subject.onNext("C")
Sure enough, it’s printed out:
--- Example of: skipUntil ---
C
Taking operators
Taking is the opposite of skipping. When you want to take elements, RxSwift has you covered. The first taking operator you’ll learn about is take, which as this marble diagram depicts, will take the first of the number of elements you specified.
Add this example to your playground to explore the first of the take operators:
example(of: "take") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5, 6)
// 2
.take(3)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an observable of integers.
- Take the first
3elements usingtake.
What you take is what you get. The output is:
--- Example of: take ---
1
2
3
The takeWhile operator works similarly to skipWhile, except you’re taking instead of skipping.
Additionally, if you want to reference the index of the element being emitted, you can use the enumerated operator. It yields tuples containing the index and element of each emitted element from an observable, similar to how the enumerated method in the Swift Standard Library works.
Enter this new example in your playground:
example(of: "takeWhile") {
let disposeBag = DisposeBag()
// 1
Observable.of(2, 2, 4, 4, 6, 6)
// 2
.enumerated()
// 3
.takeWhile { index, integer in
// 4
integer.isMultiple(of: 2) && index < 3
}
// 5
.map(\.element)
// 6
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
From the top, you:
- Create an observable of integers.
- Use the
enumeratedoperator to get tuples containing the index and value of each element emitted. - Use the
takeWhileoperator, and destructure the tuple into individual arguments. - Pass a predicate that will take elements until the condition fails.
- Use
map— which works just like the Swift Standard Librarymap— to reach into the tuple returned fromtakeWhileand get theelement. - Subscribe to and print out
nextelements.
Note: You’ll learn more about the
mapoperator in Chapter 7, “Transforming Operators.”
The result is you only receive elements as long as the integers are even, up to when the element’s index is 3 or greater.
--- Example of: takeWhile ---
2
2
4
Conversely to takeWhile, there is a takeUntil operator that will take elements until the predicate is met. It also takes a behavior argument for its first parameter that specifies if you want to include or exclude the last element matching the predictate.
Add this new example to your playground to see how this works:
example(of: "takeUntil") {
let disposeBag = DisposeBag()
// 1
Observable.of(1, 2, 3, 4, 5)
// 2
.takeUntil(.inclusive) { $0.isMultiple(of: 4) }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
With this code, you:
- Create an Observable of sequential integers.
- Use the
takeUntiloperator with inclusive behavior.
This code prints the elements up to and including the one that passes the predicate:
--- Example of: takeUntil ---
1
2
3
4
Now, change the behavior from .inclusive to .exclusive, and run the playground again. This time, the element that passes the predicate is excluded:
--- Example of: takeUntil ---
1
2
3
Like skipUntil, there is a variation of takeUntil also works with a trigger observable. The following marble diagram shows that takeUntil takes from the source observable until the trigger observable emits an element.
Add this new example, which is just like the skipUntil example you created earlier:
example(of: "takeUntil trigger") {
let disposeBag = DisposeBag()
// 1
let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()
// 2
subject
.takeUntil(trigger)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
// 3
subject.onNext("1")
subject.onNext("2")
}
Here’s what you did:
- Create a primary subject and a trigger subject.
- Use
takeUntil, passing thetriggerthat will causetakeUntilto stop taking once it emits. - Add a couple of elements onto
subject.
The elements are printed out, because takeUntil is in taking mode:
--- Example of: takeUntil trigger ---
1
2
Now add an element onto trigger, followed by another element onto subject:
trigger.onNext("X")
subject.onNext("3")
The X stops the taking, so 3 is not allowed through and nothing more is printed.
There is a way to use takeUntil with an API from the RxCocoa library to dispose of a subscription, instead of adding it to a dispose bag. You’ll learn about RxCocoa in Section III, “iOS Apps with RxCocoa.” Generally speaking, the safe bet to avoid leaking memory is to always add your subscriptions to a dispose bag. However, for the sake of completeness, here’s an example of how you would use takeUntil with RxCocoa — don’t enter this into your playground, because it won’t compile:
_ = someObservable
.takeUntil(self.rx.deallocated)
.subscribe(onNext: {
print($0)
})
In the above code, the deallocation of self is the trigger that causes takeUntil to stop taking, where self is typically a view controller or view model.
Distinct operators
The next couple of operators let you prevent duplicate contiguous items from getting through. As shown in this marble diagram, distinctUntilChanged only prevents duplicates that are right next to each other, so the second 1 gets through.
Add this new example to your playground:
example(of: "distinctUntilChanged") {
let disposeBag = DisposeBag()
// 1
Observable.of("A", "A", "B", "B", "A")
// 2
.distinctUntilChanged()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
What you do with this code:
- Create an observable of letters.
- Use
distinctUntilChangedto prevent sequential duplicates from getting through.
The distinctUntilChanged operator only prevents contiguous duplicates, so the second A and second B are prevented because they are equal to their previous element. However, the third A is allowed through because it is not equal to its previous element. This is printed as a result:
--- Example of: distinctUntilChanged ---
A
B
A
These are instances of String, which conform to Equatable. However, you can optionally use distinctUntilChanged(_:) to provide your own custom logic to test for equality; the parameter you pass is a comparer.
In the following marble diagram, objects with a property named value are being compared for equality based on value.
Add this slightly more elaborate example of distinctUntilChanged(_:) to your playground:
example(of: "distinctUntilChanged(_:)") {
let disposeBag = DisposeBag()
// 1
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
// 2
Observable<NSNumber>.of(10, 110, 20, 200, 210, 310)
// 3
.distinctUntilChanged { a, b in
// 4
guard
let aWords = formatter
.string(from: a)?
.components(separatedBy: " "),
let bWords = formatter
.string(from: b)?
.components(separatedBy: " ")
else {
return false
}
var containsMatch = false
// 5
for aWord in aWords where bWords.contains(aWord) {
containsMatch = true
break
}
return containsMatch
}
// 6
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
From the top, you:
-
Create a number formatter to spell out each number.
-
Create an observable of
NSNumbers instead ofInts, so that you don’t have to convert integers when using the formatter next. -
Use
distinctUntilChanged(_:), which takes a predicate closure that receives each sequential pair of elements. -
Use
guardto conditionally bind the element’s components separated by an empty space, or else returnfalse. -
Iterate every word in the first array and see if its contained in the second array.
-
Subscribe and print out elements that are considered distinct based on the comparing logic you provided.
As a result, only the distinct integers are printed, taking into account that in each pair of integers, one does not contain any of the word components of the other.
--- Example of: distinctUntilChanged(_:) ---
10
20
200
The distinctUntilChanged(_:) operator is also useful when you want to distinctly prevent duplicates for types that do not conform to Equatable.
Challenge
Challenges help solidify what you just learned. There are starter and finished versions of the challenge in the exercise files download.
Challenge: Create a phone number lookup
Run ./bootstrap.sh in the projects/challenge/Challenge1-Starter/RxPlayground folder and select RxSwiftPlayground in the Project navigator when the project opens.
Breaking down this challenge, you’ll need to use several filter operators. Here are the requirements, along with some suggestions:
- Phone numbers can’t begin with
0— useskipWhile. - Each input must be a single-digit number — use
filterto only allow elements that are less than10. - Limiting this example to U.S. phone numbers, which are 10 digits, take only the first
10numbers — usetakeandtoArray.
Note: The
toArrayoperator returns aSingle, which you learned about in Chapter 2, “Observables.” The convenience syntax to subscribe to aSingleissubscribe(onSuccess:onError:). If you only want to handle receiving the element if the single is successful, only implement theonSuccesshandler.
Review the setup code in the starter project. There you’ll find the following:
- A simple contacts dictionary:
let contacts = [
"603-555-1212": "Florent",
"212-555-1212": "Shai",
"408-555-1212": "Marin",
"617-555-1212": "Scott"
]
- A utility function that will return a formatted phone number from an array of
10values:
func phoneNumber(from inputs: [Int]) -> String {
var phone = inputs.map(String.init).joined()
phone.insert("-", at: phone.index(
phone.startIndex,
offsetBy: 3)
)
phone.insert("-", at: phone.index(
phone.startIndex,
offsetBy: 7)
)
return phone
}
- A publish subject to start you off:
let input = PublishSubject<Int>()
- A series of
onNextcalls to test your solution:
input.onNext(0)
input.onNext(603)
input.onNext(2)
input.onNext(1)
// Confirm that 7 results in "Contact not found",
// and then change to 2 and confirm that Shai is found
input.onNext(7)
"5551212".forEach {
if let number = (Int("\($0)")) {
input.onNext(number)
}
}
input.onNext(9)
Because this challenge is focused on using filter operators, here’s code you can use in the subscription’s next event handler to take the result from phoneNumber(from:) and print out the contact if found or else "Contact not found":
if let contact = contacts[phone] {
print("Dialing \(contact) (\(phone))...")
} else {
print("Contact not found")
}
Add your code right below the comment // Add your code here.
Once you’ve implemented your solution, follow the instructions in the comment beginning // Confirm that 7 results in... to confirm that your solution works.
Good luck!