Let’s see how this can be achieved using Combine. Below is a simple example to demonstrate the key points. We are going to use JSON Placeholder to fetch a ToDo by identifier. We need a model object, a function which takes an identifier and returns a formatted url request, and another function which takes a request and returns a data task publisher.
public struct ToDo: Decodable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
public func request(for id: Int) -> URLRequest {
URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/\(id)")!)
}
public func dataPublisher(for request: URLRequest) -> AnyPublisher<Data, URLError> {
URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.eraseToAnyPublisher()
}
We can now use these to experiment with how requests are made, simulating hitting our network request multiple times in succession with different ToDo identifiers. You can easily imagine a real world situation where this could happen. A common scenario is search, where typing into a search bar triggers network requests and you only want the response from the latest string typed. Checkout this post for an example using search.
Back to ToDo’s. Let’s try the simplest method of making new requests for different identifiers. By using subjects in playgrounds, we can prototype this logic without needing to build any UI to trigger the requests.
var subscriptions = Set<AnyCancellable>()
let fireAllSubject = PassthroughSubject<Int, Never>()
fireAllSubject
.map { request(for: $0) }
.flatMap { dataPublisher(for: $0) }
.decode(type: ToDo.self, decoder: JSONDecoder())
.replaceError(with: .empty)
.sink { print("(fire all) todo:", $0) }
.store(in: &subscriptions)
fireAllSubject.send((1))
fireAllSubject.send((2))
fireAllSubject.send((3))
fireAllSubject.send((4))
fireAllSubject.send((5))
fireAllSubject.send((6))
fireAllSubject.send((7))
We use a PassthroughSubject to simulate user interaction which triggers an event related to a specific ToDo identifier. This is map’d into a url request and then flatMap’d to a data task publisher which performs the network request. The response is decoded into a ToDo model and for simplicity any errors are simply replaced with an empty ToDo. A total of 7 requests are fired in quick succession, which results in the output below.
(fire all) todo: ToDo(userId: 1, id: 3, title: "fugiat veniam minus", completed: false)
(fire all) todo: ToDo(userId: 1, id: 2, title: "quis ut nam facilis et officia qui", completed: false)
(fire all) todo: ToDo(userId: 1, id: 7, title: "illo expedita consequatur quia in", completed: false)
(fire all) todo: ToDo(userId: 1, id: 6, title: "qui ullam ratione quibusdam voluptatem quia omnis", completed: false)
(fire all) todo: ToDo(userId: 1, id: 5, title: "laboriosam mollitia et enim quasi adipisci quia provident illum", completed: false)
(fire all) todo: ToDo(userId: 1, id: 4, title: "et porro tempora", completed: true)
(fire all) todo: ToDo(userId: 1, id: 1, title: "delectus aut autem", completed: false)
So all 7 requests are fired and the sink operator is called with the response 7 times in a completely different order to which the requests were made. This is not ideal. What we want is for only the most recent request to be processed and for any requests that are still in-flight to be cancelled. These types of situations are where the beauty and simplicity of reactive programming really shines. The only change to make is 2 lines of code. Rather than flatMap into the data task publisher, simply change this to map and then use the power of switchToLatest() to cancel any requests that have not yet completed. 😎
let fireRecentSubject = PassthroughSubject<Int, Never>()
fireRecentSubject
.map { request(for: $0) }
.map { dataPublisher(for: $0) }
.switchToLatest()
.decode(type: ToDo.self, decoder: JSONDecoder())
.replaceError(with: .empty)
.sink { print("(fire recent) todo:", $0) }
.store(in: &subscriptions)
fireRecentSubject.send((1))
fireRecentSubject.send((2))
fireRecentSubject.send((3))
fireRecentSubject.send((4))
fireRecentSubject.send((5))
fireRecentSubject.send((6))
fireRecentSubject.send((7))
Now the output is just a single ToDo with an identifier of 7. This is from the most recent request that was made!
(fire recent) todo: ToDo(userId: 1, id: 7, title: "illo expedita consequatur quia in", completed: false)
Feel free to grab this playground from Github. I absolutely ❤️ reactive programming and how complex scenarios can be solved with simple reactive operator chains. We touched on some very basic error handling today, but stay tuned for more of a deep dive into how errors should be handled in Combine. 🚜