Using .switchToLatest()

Sponsored

Codemagic logo
Codemagic CI/CD for mobile teams

What do you get when you put love for iOS and DevOps together? Answer: Codemagic CI/CD

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.

DataFetcher.swift
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
}
  1. First, we need to create a new publisher by using the built-in dataTaskPublisher and we provide it with a URLRequest.
  2. Then, we get the data field using the map operator and then we decode it using the decode operator and passing it a type that conforms to the Decodable protocol and the decoder we want to use.
  3. This step is very important for what we want to achieve later. The Publisher.DataTaskPublisher returns a failure type of URLError 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 the switchToLatest operator later on.
  4. 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:

DataFetcher.swift
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:

  1. We create a publisher with a Notification.Name. This is what will determine which notifications we react to and which we don’t.
  2. We use the flatMap operator to replace the publisher with our request publisher. We use the information from the Notification provided by the upstream publisher to pass the user id to the getNewUserProfile method.
  3. We assign the property reutrned by the request publisher to a property in our class called user.
  4. Finally, we store the resulting Cancellable in a set. This is similar to RxSwift’s DisposeBag.

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:

DataFetcher.swift
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:

  1. Because of the nature of switchToLatest, we use map 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.
  2. 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!