Combine — switchToLatest()

switchToLatest() is one of the most powerful operator in Combine as it enables to switch entire publisher subscriptions on the fly while canceling the pending publisher subscription, thus switching to the latest one.

Only publishers that emit publishers as Output can use the switchToLatest() operator.

AnyPublisher<AnyPublisher<[Post], Never>, Never> 

Every time a publisher receives a new Publisher as output, the previous subscriptions from that stream will automatically be cancelled.

The operator is highly useful when it comes to chaining operations with a network request.

— Use case

When the user taps a view, we want to trigger a network request that sends us back a list of posts.

First let’s implement a Webservice class to model our request.

struct Post: Decodable {
    let userId, id: Int
    let title, body: String
}

final class Webservice {
    func fetch(request: Int) -> AnyPublisher<[Post], Never> {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .delay(for: .seconds(Int.random(in: 3...6)), scheduler: DispatchQueue.main)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .handleEvents(receiveOutput: { _ in print("This is response from request n° \(request) ") })
            .eraseToAnyPublisher()
    }
}

For testing purpose, we use the delay() operator to model our requests with different response times and print the response with the request number to track our different network calls.

Now, we need to handle the user tap gesture through a custom implementation of the Subscription and Publisher protocols.

Check out my article Combine - Handling UIKit’s gestures to see the full implementation.

class ViewController: UIViewController {
    var cancellables = Set<AnyCancellable>()
    var requestCount = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        view.gesture()
            .handleEvents(receiveOutput: { _ in self.requestCount += 1 })
            .flatMap { _ in Webservice().fetch(request: self.requestCount) }
            .sink(receiveValue: { _ in })
            .store(in: &cancellables)

    }   
}

When a tap gesture is detected, we call the fetch() method from our Webservice class to retrieve the posts from the user.
We use flatMap() to trigger the request and sink() to create our subscriber.

Let’s tap our view five times and check the console.

// This is response from request n° 1 
// This is response from request n° 3 
// This is response from request n° 4 
// This is response from request n° 2 
// This is response from request n° 5

Five network requests are performed which is quite unnecessary (and potentially expensive) since we really are interested in the latest subscription only.

We need to use switchToLatest() to cancel previous subscriptions.

view.gesture()
    .handleEvents(receiveOutput: { _ in self.requestCount += 1 })
    .map { _ in Webservice().fetch(request: self.requestCount) }
    .switchToLatest() // A new `Publisher` output will cancel the previous subscriptions.
    .sink(receiveValue: { _ in })
    .store(in: &cancellables)

Let’s tap five times again and check out the console.

// This is response from request n° 5 

Since previous subscriptions have been cancelled, the network requests didn’t go through giving us a one and only print statement from our latest publisher.