Using .switchToLatest()
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
When Apple introduced Combine in WWDC 2019, an Apple Functional Reactive Programming (FRP) framework, they also introduced a bunch of functional operators too. In this post, we’re going to talk about an operator that I have been using quite a lot recently and, in my opinion, is one of the most powerful within the Combine
API.
Note that this post assumes that you already have some experience with Functional Reactive Programming and Combine. If you would like to learn more, check out this awesome article by John Sundell on Combine.
The Problem
To demonstrate how to use the switchToLatest
operator we’re going to look at an example using the publishers that are already built-in in the NotificationCenter
API.
The functionality we want to build is to fetch some data from our BE service or our 3rd-party API every time a user has logged in. In order to achieve this and as we need to trigger this in a few places in our app, we’re going to use a custom notification as the trigger.
The network request publisher
Let’s get started by building the request publisher that will be used to fetch the User
object from the BE.
func getNewUserProfile(userID: String?) -> AnyPublisher<User, Never> {
URLSession
.shared
.dataTaskPublisher(for: getUserURL(for: userID)) // 1
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder()) // 2
.replaceError(with: User.anonymous) // 3
.eraseToAnyPublisher() // 4
}
- First, we need to create a new publisher by using the built-in
dataTaskPublisher
and we provide it with aURLRequest
. - Then, we get the data field using the
map
operator and then we decode it using thedecode
operator and passing it a type that conforms to theDecodable
protocol and the decoder we want to use. - This step is very important for what we want to achieve later. The
Publisher.DataTaskPublisher
returns a failure type ofURLError
and to handle this, as we are only interested in performing an action if the response returns a user object, with replace it with an anonymous (or logged out) user. This will allow us to use theswitchToLatest
operator later on. - We erase the type of the publisher to
AnyPublisher
.
Reacting to notifications
Now that we have built our network service method, the next thing we have to do is call this endpoint every time that a specific notification is received. To do this, we will create a custom notification named user-sign-in
and we will use the built-in publisher
method in the NotificationCenter
API to listen for events:
var cancellables = Set<AnyCancellable>()
NotificationCenter
.default
.publisher(for: Notification.Name("user-sign-in")) // 1
.flatMap { getNewUserProfile(id: $0.userInfo?["id"] as? String) } // 2
.assign(to: \.user) // 3
.store(in: &cancellables) // 4
Now that we have seen how to trigger the request from a notification, let’s explain what’s happening in the snippet above:
- We create a
publisher
with aNotification.Name
. This is what will determine which notifications we react to and which we don’t. - We use the
flatMap
operator to replace the publisher with our request publisher. We use the information from theNotification
provided by the upstream publisher to pass the user id to thegetNewUserProfile
method. - We assign the property reutrned by the request publisher to a property in our class called
user
. - Finally, we store the resulting
Cancellable
in a set. This is similar toRxSwift
’sDisposeBag
.
Just like that, we have all the logic we need. Now, every time we trigger the notifcation using the post
method in NotificationCentre
, a new network request will be triggered and a new User
object will be received by the subscriber.
All of this is great, but what happens if multiple notifications happen in a short space of time? Will that trigger a lot of unnecessary requests that will never be cancelled even if there is a more recent one? The answer to all these questions is yes, and if the upstream logic is expensive, then your app performance will be badly affected.
Using switchToLatest
instead
Luckily for us, we have an operator that can take care of all the cancelling operations for us. In the example we are looking at in this blog, we only care about the latest request and we want any previous lingering requests that have not been fulfilled to be cancelled and the resources to be freed up. Let’s look at the example from the previous section, but this time using switchToLatest
instead:
var cancellables = Set<AnyCancellable>()
NotificationCenter
.default
.publisher(for: Notification.Name("user-sign-in"))
.map { getNewUserProfile(id: $0.userInfo?["id"] as? String) } // 1
.switchToLatest() // 2
.assign(to: \.user)
.store(in: &cancellables)
Let’s look at how specifically the snippet above works:
- Because of the nature of
switchToLatest
, we usemap
here to convert the stream to a publisher of publishers type, so that further down the stream, they can be switched as new values come in. - Using
switchToLatest
means that we can switch publishers on the fly, cancelling all previous subscriptions and switching to the latest publisher. The importance of this is notable as if a notification is received before the previous request has ended, this will be cancelled and only the new one will be processed.
Just like that we have built an efficient stream that reacts to notifications and requests data only when needed and cancelling all unnecessary requests!