6.
Filtering Operators in Practice
Written by Marin Todorov
In the previous chapter, you began your introduction to the functional aspect of RxSwift. The first batch of operators you learned about helped you filter the elements of an observable sequence.
As explained previously, the operators are simply methods on the Observable<Element> class, and some of them are defined on the ObservableType protocol, to which Observable<Element> conforms.
The operators operate on the elements of their Observable class and produce a new observable sequence as a result. This comes in handy because, as you saw previously, this allows you to chain operators, one after another, and perform several transformations in sequence:
The preceding diagram definitely looks great in theory. In this chapter, you’re going to try using the filtering operators in a real-life app. In fact, you are going to continue working on the Combinestagram app that you already know and love from Chapter 4, “Observables and Subjects in Practice.”
Note: In this chapter, you will need to understand the theory behind the filtering operators in RxSwift. If you haven’t worked through Chapter 5, “Filtering Operators,” do that first and then come back to the current chapter.
Without further ado, let’s have a look at putting filter, take, and company to work!
Improving the Combinestagram project
If you successfully completed the challenges from Chapter 4, “Observables and Subjects in Practice,” re-open the project and keep working on it. Otherwise, you can use the starter project provided for this chapter.
It’s important that you have a correct solution to the challenge in Chapter 4, since it plays a role in one of the tasks in this chapter. If you’re in doubt, just consult UIAlertViewController+Rx.swift in the provided starter project and compare it to your own solution.
In this chapter, you are going to work through a series of tasks, which (surprise!) will require you to use different filtering operators. You’ll use different ones and see how you can use counterparts like skip and take. You’ll also learn how to achieve similar effect by using different operators, and finally, you will take care of a few of the issues in the current Combinestagram project.
Note: Since this book has only covered a few operators so far, you will not write the “best possible” code. For this chapter, don’t worry about best practices or proper architecture yet, but instead focus on truly understanding how to use the filtering operators. In this book, you’re going to slowly build up towards writing good RxSwift code. It’s a process!
Refining the photos’ sequence
Currently the main screen of the app looks like this:
The app works for the most part, but if you play with it for a while, you will certainly notice some shortcomings. And, honestly, it could do with some new and fresh features as well.
For example, once the user has added a batch of photos to their collage, you might want to do more than simply regenerate the preview each time. At the point when the photos observable completes, the user will be coming back to the main screen; there might be things to turn on or off, labels to update, or more. You’ll take a look next at how to “do more things” by sharing a subscription to the same Observable instance.
Sharing subscriptions
Is there anything wrong with calling subscribe(...) on the same observable multiple times? Turns out there might be!
I’ve already mentioned that observables are lazy, pull-driven sequences. Simply calling a bunch of operators on an Observable doesn’t involve any actual work. The moment you call subscribe(...) directly on an observable or on one of the operators applied to it, that’s when the Observable livens up and starts producing elements.
To do that, the observable calls its create closure each time you subscribe to it. in some situations, this might produce some bedazzling effects!
Take a look at the code below; you can type it in a Playground if you want to follow:
let numbers = Observable<Int>.create { observer in
let start = getStartNumber()
observer.onNext(start)
observer.onNext(start+1)
observer.onNext(start+2)
observer.onCompleted()
return Disposables.create()
}
The code creates an Observable<Int>, which produces a sequence of three numbers: start, start+1, start+2.
Now see what getStartNumber() looks like:
var start = 0
func getStartNumber() -> Int {
start += 1
return start
}
The function increments a variable and returns it; nothing can go wrong there. Or can it? Add a subscription to numbers and see for yourself:
numbers
.subscribe(
onNext: { el in
print("element [\(el)]")
},
onCompleted: {
print("-------------")
}
)
You will get the exact output you expected. Yay!
element [1]
element [2]
element [3]
-------------
Copy and paste the exact same subscription code one more time though, and this time the output is different.
element [1]
element [2]
element [3]
-------------
element [2]
element [3]
element [4]
-------------
The problem is that each time you call subscribe(...), this creates a new Observable for that subscription — and each copy is not guaranteed to be the same as the previous. And even when the Observable does produce the same sequence of elements, it’s overkill to produce those same duplicate elements for each subscription. There’s no point in doing that.
To share a subscription, you can use the share() operator. A common pattern in Rx code is to create several sequences from the same source Observable by filtering out different elements in each of the results.
You’ll use share in a practical example in Combinestagram to understand its purpose a bit better.
Open the starter project and select MainViewController.swift. Scroll to actionAdd() and replace the line photosViewController.selectedPhotos with the following, which creates a new variable called newPhotos to subscribe to (rather than subscribing to selectedPhotos value directly):
let newPhotos = photosViewController.selectedPhotos
.share()
newPhotos
[ here the existing code continues: .subscribe(...) ]
Now, instead of each subscription creating a new Observable instance like so:
You allow for multiple subscriptions to consume the elements that a single Observable produces for all of them, like so:
Now you can create a second subscription to newPhotos and filter out some of the elements you don’t need.
Before moving on though, it’s important to learn a bit more about how share works.
share (and its specializations via parameters) create a subscription only when the number of subscribers goes from 0 to 1 (i.e., when there isn’t a shared subscription already). When a second, third and so on subscribers start observing the sequence, share uses the already created subscription to share with them. If all subscriptions to the shared sequence get disposed (e.g. there are no more subscribers), share will dispose the shared sequence as well. If another subscriber starts observing, share will create a new subscription for it just like described above.
Note:
share()does not provide any of the subscriptions with values emitted before the subscription takes effect.share(replay:scope:), on the other hand, keeps a buffer of the last few emitted values and can provide them to new observers upon subscription.
The rule of thumb about sharing operators is that it’s safe to use share() with observables that do not complete, or if you guarantee no new subscriptions will be made after completion. If you want piece of mind, use share(replay: 1) - you’ll learn more about this in Chapter 8, “Transforming Operators in Practice.”
Ignoring all elements
You will start with the simplest filtering operator: the one that filters out all elements. No matter your value or type, ignoreElements() says “You shall not pass!”
Recall that newPhotos emits a UIImage element each time the user selects a photo. In this section, you are going to add a small preview of the collage in the top-left corner of the screen — a navigation icon, if you will.
Since you would like to update that icon only once, when the user returns to the main view controller, you need to ignore all UIImage elements and act only on a .completed event.
ignoreElements() is the operator that lets you do just that: it discards all elements of the source sequence and lets through only .completed or .error.
In fact, if you paid attention while working through Chapter 4, “Observables in Practice,” you’d remember that there is a special type of Observable that emits on elements except for .completed and .error - Completable.
Indeed, the ignoreElements() operator transforms a generic observable sequence to a Completable by filtering out any .next elements.
At the end of actionAdd(), insert the following (Xcode will display an error but you will fix it in a moment):
newPhotos
.ignoreElements()
.subscribe(onCompleted: { [weak self] in
self?.updateNavigationIcon()
})
.disposed(by: bag)
This subscription to newPhotos will ignore all images and will run the onCompleted closure when the user returns to the main view controller. To silence the Xcode error, add the missing method anywhere in the MainViewController class:
private func updateNavigationIcon() {
let icon = imagePreview.image?
.scaled(CGSize(width: 22, height: 22))
.withRenderingMode(.alwaysOriginal)
navigationItem.leftBarButtonItem = UIBarButtonItem(image: icon,
style: .done, target: nil, action: nil)
}
Run the app, and make a new collage. Each time you come back from adding photos, your new subscription updates the mini-preview in the top-left corner.
Filtering elements you don’t need
Of course, as great as ignoreElements() is, sometimes you will need to ignore just some of the elements — not all of them.
In those cases, you will use filter(_:) to let some elements through and discard others.
For example, you might have noticed that photos in portrait orientation do not fit very well in the collages in Combinestagram.
Of course, you could write smarter collage-building code, but in this chapter you’re going to discard portrait photos and only include landscapes instead. That’s one way to solve the issue. Pretend it’s a feature, and not a bug!
Scroll to the top of actionAdd() and alter the first subscription to newPhotos. For the first operator, insert a filter:
newPhotos
.filter { newImage in
return newImage.size.width > newImage.size.height
}
[existing code .subscribe(...)]
Now each photo that newPhotos emits will have to pass a test before it gets to the subscriber. Your filter operator will check if the width of the image is larger than its height, and if so, it will let it through. Photos in portrait orientation will be discarded.
Run the app and try adding some photos from your device’s Camera Roll. No matter how many times you tap on any photo in portrait orientation, it will not be added to the collage.
Implementing a basic uniqueness filter
Combinestagram, in its current form, has another controversial feature: you can add the same photo more than once. That doesn’t make for very interesting collages, so in this section you’ll add some advanced filtering to prevent the user from adding the same photo multiple times.
Note: There are better ways to achieve the required result than what you are going to implement below. It is, however, a great exercise to build a solution with your current RxSwift skill set.
Observables don’t provide a current state or a value history. Therefore, to check if emitted elements are unique, you need to somehow keep track of them yourself.
Keeping an index of emitted images is not going to help you, since two UIImage objects representing the same image aren’t equal. The best method is to store a hash of the image data or the asset URL, but in this simple exercise, you are going to use the byte length of the image. This will not guarantee the uniqueness of the image’s index, but it’ll help you build a working solution without going too deep into the implementation details.
Add a new property to the MainViewController class:
private var imageCache = [Int]()
You will store the length in bytes of each image in this array, and will look it up for each incoming image. Scroll further down and insert another filter, just below the filter you added last:
[existing .filter {newImage in ... ]
.filter { [weak self] newImage in
let len = newImage.pngData()?.count ?? 0
guard self?.imageCache.contains(len) == false else {
return false
}
self?.imageCache.append(len)
return true
}
[existing code .subscribe(...)]
Inside the filter’s closure you get the PNG data for the new image and store its byte count as the constant len. If imageCache contains a number with the same value, you assume the image is not unique and discard it by returning false.
If the image is unique for the collage, you store its byte length in imageCache and return true.
Note: In this example, you introduce state (namely
imageCache) in your otherwise neat and lean code. Don’t worry too much about it: in Chapter 9, “Combining Operators,” you will learn about thescanoperator, which helps you solve these kinds of situations.
To nicely wrap up this feature, add the following to actionClear():
imageCache = []
This will clear your image cache and ensure the user can re-use the photos for their next collage.
Run the app and give your new feature a try by tapping few times on the same photo. You will see that the photo is added to the collage just once.
Congratulations — that was quite a complex filtering you just accomplished!
Keep taking elements while a condition is met
One of the “best” bugs in Combinestagram is that the + button is disabled if you add six photos, which prevents you from adding any more images. But if you are in the photos view controller, you can add as many as you wish. There ought to be a way to limit those, right?
Well, believe it or not, you can easily filter all elements after a certain condition has been met by using the takeWhile(_) operator. You provide a boolean condition, and takeWhile(_) discards all elements when this condition evaluates to false.
Scroll again towards the top of actionAdd(), find the line newPhotos of the first subscription and add the following code just below that line:
newPhotos
.takeWhile { [weak self] image in
let count = self?.images.value.count ?? 0
return count < 6
}
[existing code: filter {...}]
takeWhile(...) will let photos through as long as the total number of images in the collage is less than 6. You use the ?? nil coalescing operator to default to 0 if self is nil. This is to satisfy the compiler and avoid force-unwrapping self.
Run the app and try to add lots photos to the collage. Once you add 6 photos, you won’t be able to add any more. Mission accomplished!
Note: In the code above you access a property of your view controller directly, which is a somewhat controversial practice in reactive programming. In Chapter 9, “Combining Operators,” you will learn how to combine multiple observable sequences so that you don’t have to use the view controller to keep state.
Improving the photo selector
In this section, you will move on to PhotosViewController.swift. First, you are going to build a new custom Observable, and then (surprise!) filter it in different ways to improve the user experience on that screen.
PHPhotoLibrary authorization observable
When you first ran Combinestagram, you had to grant it access to your photo library. Do you remember if the user experience was flawless in that moment? Probably not. You were probably overwhelmed at the time with operators, observable sequences, and the like.
The very first time your app tries to access the device’s photo library, the OS will asynchronously ask for the user’s permission. That happens just once: the very first time you run the app. Therefore, for this section you will need to remove Combinestagram from your simulator every time you want to get the authorization dialog again.
If you decide to follow the chapter exactly, do the following: bring the iPhone Simulator to the front. Long-press the Combinestagram icon until the X icon appears on the top-left and your icons start to wiggle. Then, tap to remove the Combinestagram app before reinstalling it.
Run Combinestagram and tap on +; the access alert box will pop up. When you tap OK, you’ll see that the photos don’t show up automatically. If you go back to the main view controller and tap + again, the photos appear. Hm…
Let’s see what the problem is and how can you solve it. In PhotosViewController, you load all photos in a property named photos. There currently is no way to reload photos once the access has been granted.
Create a new source file and name it PHPhotoLibrary+Rx.swift. Add the following inside:
import Foundation
import Photos
import RxSwift
extension PHPhotoLibrary {
static var authorized: Observable<Bool> {
return Observable.create { observer in
return Disposables.create()
}
}
}
This adds a new Observable<Bool> property named authorized on PHPhotoLibrary. Nothing you haven’t done before.
This observable can go two separate ways, depending on whether the user has already granted access:
Let’s recreate the logic from the flowchart above in code. Inside the create closure in your code, insert the following just above the line: return Disposables.create():
DispatchQueue.main.async {
if authorizationStatus() == .authorized {
observer.onNext(true)
observer.onCompleted()
} else {
observer.onNext(false)
requestAuthorization { newStatus in
observer.onNext(newStatus == .authorized)
observer.onCompleted()
}
}
}
If the user has previously granted access, the code instantly emits a true value. Otherwise, the code asks for user permission and emits the result: true if access was granted, or false in any other case.
A note on the usage of DispatchQueue.main.async {...}: generally, your observables should not block the current thread because that could block your UI, prevent other subscriptions, or have other nasty consequences.
Now that you’ve built a fancy new observable sequence, it’s time to divide and conquer… erm… I mean filter and observe.
Reload the photos collection when access is granted
You have two scenarios in which you end up having access to the photo library:
- On a first run of the app, the user taps OK in the alert box:
- On any subsequent run of the app if access has been previously granted:
The first thing you are going to do is subscribe to PHPhotoLibrary.authorized. true can only be the last element in that particular sequence, so whenever you get a true element that means you can reload the collection and display the Camera Roll photos onscreen.
Open PhotosViewController.swift and before writing any of the logic, add a new dispose bag to the current view controller:
private let bag = DisposeBag()
Next, in viewDidLoad(), add:
let authorized = PHPhotoLibrary.authorized
.share()
Here, you create a new shared observable and name it authorized. You do this because you will create two separate subscriptions to that Observable.
As this section’s task, you will wait for a true element. When you encounter one, you will reload the photos and the collection view. Add this code to viewDidLoad():
authorized
.skipWhile { !$0 }
.take(1)
.subscribe(onNext: { [weak self] _ in
self?.photos = PhotosViewController.loadPhotos()
DispatchQueue.main.async {
self?.collectionView?.reloadData()
}
})
.disposed(by: bag)
In this code, you use two filtering operators one after another. First you use skipWhile(_:) to ignore all false elements. In case the user doesn’t grant access, your subscription’s onNext code will never get executed.
Secondly, you chain another operator: take(1). Whenever a true comes through the filter, you take that one element, ignore everything else after it, and complete the sequence.
In this particular sequence, true is always the last element so there is no screaming need to use take(1). But using a take(1) clearly expresses your intention, and if the permission mechanism changes later on, your subscription will still do exactly what you wanted: on the first true element, it will reload the collection view and ignore anything that comes afterwards.
Inside the subscribe(...) closure you switch to the main thread before reloading the collection view. Why do you need to do that? If you look up the source code for PHPhotoLibrary.authorized, here’s where you emit the true value after the user has tapped OK to grant access:
requestAuthorization { newStatus in
observer.onNext(newStatus == .authorized)
}
requestAuthorization(_:) doesn’t guarantee on which thread your completion closure will be executed, so it might fall on a background thread. You call onNext(_:), which invokes all the subscription code to the observable on the same thread. Finally, in your subscription you call self?.collectionView?.reloadData(), and if you’re still on the background thread, UIKit will crash.
When you update the UI, you need to be sure you’re on the main thread.
Note: Threading is always important in asynchronous programming, and if anything, RxSwift makes it easier to tame your threads. In RxSwift code, you aren’t encouraged to use GCD to switch threads; you should use Schedulers instead. You will learn more about this in Chapter 15, “Intro to Schedulers and Threading in Practice.”
Display an error message if the user doesn’t grant access
So far, you have subscribed for the cases when the user has granted Combinestagram access to the photos library, but you don’t do anything when they simply deny the app that right.
Here are all the possible outcomes when the app doesn’t have access:
- On the first run of the app, the user taps on Don’t Grant in the access alert box:
- On any subsequent run if the user has previously denied access:
The sequence elements are the same in both cases because they fall in the same code path. What you can see from the two sequences above is a pattern:
-
You can always ignore the first element from the sequence, since it’s never the final one.
-
You then check if the last element in the sequence is
false. In that case, show an error message.
It seems easy enough! Add the following code to viewDidLoad():
authorized
.skip(1)
.takeLast(1)
.filter { !$0 }
.subscribe(onNext: { [weak self] _ in
guard let errorMessage = self?.errorMessage else { return }
DispatchQueue.main.async(execute: errorMessage)
})
.disposed(by: bag)
Now you have a bit of an operator overkill! Using skip, takeLast and filter together expresses best what you intend to do. However, it feels a bit too much, given that in this particular situation you might not need all of them.
For example, if you are using takeLast(1), doesn’t that imply you are going to skip the first element anyway? And if you are using filter to check for a false element, is it really necessary to take the last one?
As with all big questions in life, the answer is “it depends” :trollface:
With the current implementation of PHPhotosLibrary.authorized, the code below will suffice:
authorized
.skip(1)
.filter { !$0 }
You always know there will be maximum of two elements, so you skip the first and filter the following ones. But this code would also have been enough:
authorized
.takeLast(1)
.filter { !$0 }
This way you ignore everything before the last element and check if that last one is false. This is also a fine solution.
You can even involve some other filtering operators; you can replace skip and takeLast with distinctUntilChanged(). For the given possible elements, you can do the following:
authorized
.distinctUntilChanged()
.takeLast(1)
.filter { !$0 }
With this, you will achieve exactly the same effect, given the order and possible values of the current sequence. For other sequences, all of the code examples above aren’t guaranteed to produce the same result.
So in fact, you can shorten your subscription code quite a bit. But that’s if you are sure the sequence logic will never change. What about when the next iOS version comes out? Can you guarantee that the logic behind grant-access-alert-box will not change? Probably you can’t (except if you’re on the UIKit team, and in that case - hello!)
So, keeping skip, takeLast, and filter might be the best way to ensure that the app logic isn’t going to break after the next iOS version is released. Or you can keep it as-is, and make the logic of your authorized observable more deterministic so that the subscription code can be simpler.
As I said, it depends! ¯\(ツ)/¯
But for now, let’s focus on clearing that annoying error in Xcode that says errorMessage is not found. You can add that method anywhere in PhotosViewController:
private func errorMessage() {
alert(title: "No access to Camera Roll",
text: "You can grant access to Combinestagram from the Settings app")
.subscribe(onCompleted: { [weak self] in
self?.dismiss(animated: true, completion: nil)
_ = self?.navigationController?.popViewController(animated: true)
})
.disposed(by: bag)
}
You use alert(title:description:) from Challenge 1 of Chapter 4 to show an alert box. If you implemented alert(title:description:) as required, the resulting Observable will complete once the user taps the alert button. This will dispose the observable and hide the alert, and that ultimately will trigger your onCompleted code from above and pop out the photos controller.
You can try that new feature by doing the following: open the Settings app in your Simulator, scroll to the bottom, tap on Combinestagram, then set the Photos access setting to Never.
Then run the app again, and tap on the + button to trigger the complete sequence of checking for the current access authorization, invoking requestAuthorization(_:), and ultimately popping that alert on screen:
Isn’t it fascinating that the complete logic of authorization checks and UI updates is made so simple through the use of observables? I certainly find it fascinating!
Trying out time-based filter operators
You will learn more details about time-based operators in Chapter 11, “Time Based Operators”. However, some of those operators are also filtering operators. That’s why you are going to try using a couple of them in this chapter.
Time-based operators use something called a Scheduler. Schedulers are an important concept that you will learn about later in this book. For the examples below, you will use MainScheduler.instance, which is a shared scheduler object that will, alongside its other features, run your code on the main thread of your app.
Without going into more details, let’s have a look at two short examples of filtering based on time.
Completing a subscription after given time interval
Right now if the user has denied access to their photo library they see the No access alert box and they have to tap on Close to go back.
It’s a common pattern for messages that don’t necessarily require user input to disappear on their own after a while. In this section, you are going to alter your code so that you show the alert box for a maximum of 5 seconds. If the user doesn’t tap Close themselves within that time limit, you will automatically hide the alert and dispose of the subscription.
Open PhotosViewController.swift and scroll to that last method you added in there: errorMessage(). Directly after the line alert(title: ..., description: ...), insert the following:
.asObservable()
.take(.seconds(5), scheduler: MainScheduler.instance)
[existing code: .subscribe(onCompleted: ...]
take(_:scheduler:) is a filtering operator much like take(1) or takeWhile(...). take(_:scheduler:) takes elements from the source sequence for the given time period. Once the time interval has passed, the resulting sequence completes.
Note: You have to convert your
Completableto an observable viaasObservable()as thetake(_:scheduler:)operator is not available on theCompletabletype.
Now your alert box observable is going to live, at most, for five seconds (if not less) and then it will complete, thus disposing of the subscription, hiding the alert box, and popping out the current controller as per your subscription code.
In the event the user taps Close, that will complete the sequence immediately without waiting for 5 seconds and will have the same effect: hide the alert and pop the current controller out.
Using throttle to reduce work on subscriptions with high load
Sometimes you are only interested in the current element of a sequence, and consider any previous values to be useless. For a real-life example, switch to MainViewController.swift and find viewDidLoad().
Consider this part of the existing code:
images
.subscribe(onNext: { [weak imagePreview] photos in
guard let preview = imagePreview else { return }
preview.image = photos.collage(size: preview.frame.size)
})
Every time the user selects a photo, the subscription receives the new photo collection and produces a collage. As soon as you receive the new photo collection, the previous one is useless. However, if the user taps on multiple photos quickly in succession, the subscriptions will produce a new collage for each incoming element nonetheless. Producing all those intermediate collages is wasted effort; each incoming element renders the work put into creating the preceding collage futile.
But how can you know if there will be a new element incoming shortly in the future or not?
You will be surprised how often you will find yourself in the situation where you need to solve this exact problem: “if there are many incoming elements one after the other, take only the last one.” Since it’s such a common pattern of asynchronous programming, there is a special Rx operator for it.
Directly before .subscribe(onNext: ...) in the first subscription in viewDidLoad() insert the following:
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)
[existing code: .subscribe(onNext: ...]
throttle(_:scheduler:) filters any elements followed by another element within the specified time interval.
Note: 500 milliseconds equal to 0.5 seconds but the
RxTimeIntervaltype you use withthrottledoes not allow for fractions so we use 500 milliseconds instead of setting the interval in seconds.
So if the user selects a photo and taps another one after 0.2 seconds, throttle will filter the first element out and only let the second one through. This will save you the work to build the first intermediate collage, which will be immediately outdated by the second one.
Of course, throttle also works for more than one element that comes in close succession. If the user selects five photos, tapping them quickly one after the other, throttle will filter the first four and let only the 5th element through, as long as there isn’t another element following it in less than 0.5 seconds.
Here are just some of the many situations in which you can use throttle:
- You have a search text field subscription, which sends its current text to a server API. By using
throttle, you can let the user quickly type in words and only send a request to your server after the user has finished typing. - You present a modal view controller when the user taps a bar button. You can prevent double taps, which present the modal controller two times, by throttling the tap events by only accepting the last tap in double or triple tap sequences.
- The user is dragging their finger across the screen and you are interested only in the spots where they stop for a moment. You can throttle the current touch location and only consider only the elements where the current location stops changing.
throttle(_:scheduler:) is incredibly useful in situations when you are given too much input. I would love to have a throttle operator in real life, but I can dream, can’t I?
With this last exercise, you have wrapped up development on Combinestagram and completed your introduction to filtering operators.
You also tapped a little bit into upcoming material in this book. You’ve seen that taming threads is a common pattern, and I’m sure you are looking forward to the operators that will allow you to switch threads as you work on your subscriptions.
Another topic you peeked into was time-based operators. No worries though; since RxSwift is an asynchronous event-based framework, time is always on your side. And you can do more with time operators than just filtering – but you will learn more about that soon enough.
Before moving on, take time to reflect on all the code you wrote in Combinestagram, and how it simplified some of the common asynchronous programming patterns you had to deal with.
Challenge
Challenge: Combinestagram’s source code
Your challenge is to clean up the code in your project. For example, right in that last spot where you added code in MainViewController.swift’s viewDidLoad(), there are two subscriptions to the same observable. Clean that up by using a shared sequence.
Additionally, look at all subscriptions and decide if you want to replace some operators, or even remove some of them.
If you’re feeling like taking on an extra task for desert, currently the navigation bar preview doesn’t clear when you click the Clear button. Fix that any way you like.
Generally, take it easy and don’t push yourself too hard. Operators can be overwhelming if you try to take them all in at once. When you feel ready, move on to the next chapter where you will be introduced to the poster-child of reactive programming map and its weird cousin flatMap.