Reputation: 3257
Sometimes I need to make a series of network calls where each one depends on the prior ones, so they must be done in series. Typically, network calls take a completion handler as an argument. A series of calls can be done via nested completion handlers, but that gets difficult to read.
As an alternative, I've been dispatching the process to a global queue and using a Grand Central Dispatch DispatchSemaphore
to stall until the network query returns. But it doesn't seem very elegant. Here is some example code:
let networkSemaphore = DispatchSemaphore(value: 0)
var body: some View {
Text("Hello, world!")
.padding()
.onAppear { doChainOfEvents() }
}
func networkFetch(item: String, callback: @escaping (Int) -> Void) {
print("Loading \(item)...")
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(2)) {
let numLoaded = Int.random(in: 1...100)
callback(numLoaded)
networkSemaphore.signal()
}
networkSemaphore.wait()
}
func doChainOfEvents() {
DispatchQueue(label: "com.test.queue.serial").async {
networkFetch(item: "accounts") { num in
print("\(num) accounts loaded.")
}
networkFetch(item: "history") { num in
print("\(num) messages loaded.")
}
networkFetch(item: "requests") { num in
print("\(num) requests loaded")
}
print("Network requests complete. ✓")
}
print("Control flow continues while network calls operate.")
}
and the printed result of doChainOfEvents()
:
Control flow continues while network calls operate. Loading accounts... // (2 second delay) 79 accounts loaded. Loading history... // (2 second delay) 87 messages loaded. Loading requests... // (2 second delay) 54 requests loaded Network requests complete. ✓
Can you think of more elegant way to achieve this? It seems to me there ought to be one within using Grand Central Dispatch. I could use a DispatchGroup
in place of the semaphore, but I don't think it would buy me anything, and a group with one item at a time seems silly.
Upvotes: 1
Views: 1177
Reputation: 437552
With Combine you can chain a series of requests, e.g., create a property to hold the AnyCancellable
object:
var cancellable: AnyCancellable?
And then, assuming you needed “accounts” to fetch the “history”, and needed the “history” to fetch the “requests”, you could do something like:
func startRequests() {
cancellable = accountsPublisher()
.flatMap { accounts in self.historyPublisher(for: accounts) }
.flatMap { history in self.requestsPublisher(for: history) }
.subscribe(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { requests in
print(requests)
}
}
That will run the publishers sequentially, passing the value from one to the next.
It's not relevant the question at hand, but here is my mockup:
struct Account: Codable {
let accountNumber: String
}
struct History: Codable {
let history: String
}
struct Request: Codable {
let identifier: String
}
struct ResponseObject<T: Decodable>: Decodable {
let json: T
let origin: String
}
func accountsPublisher() -> AnyPublisher<[Account], Error> {
URLSession.shared
.dataTaskPublisher(for: accountRequest)
.map(\.data)
.decode(type: ResponseObject<[Account]>.self, decoder: decoder)
.map(\.json)
.eraseToAnyPublisher()
}
func historyPublisher(for accounts: [Account]) -> AnyPublisher<History, Error> {
URLSession.shared
.dataTaskPublisher(for: historyRequest)
.map(\.data)
.decode(type: ResponseObject<History>.self, decoder: decoder)
.map(\.json)
.eraseToAnyPublisher()
}
func requestsPublisher(for history: History) -> AnyPublisher<[Request], Error> {
URLSession.shared
.dataTaskPublisher(for: requestsRequest)
.map(\.data)
.decode(type: ResponseObject<[Request]>.self, decoder: decoder)
.map(\.json)
.eraseToAnyPublisher()
}
var accountRequest: URLRequest = {
let url = URL(string: "http://httpbin.org/post")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONEncoder().encode([Account(accountNumber: "123"), Account(accountNumber: "789")])
return request
}()
var requestsRequest: URLRequest = {
let url = URL(string: "http://httpbin.org/post")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONEncoder().encode([Request(identifier: "bar"), Request(identifier: "baz")])
return request
}()
var historyRequest: URLRequest = {
let url = URL(string: "http://httpbin.org/post")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try! JSONEncoder().encode(History(history: "foo"))
return request
}()
Now, in the above example, I am using httpbin.org/post
to parrot back data to me under the json
key. Obviously, this is not a realistic scenario, but it illustrates the pattern of chaining various publishers together (and using Combine to get out of the weeds of writing imperative code).
So do not get lost in the weeds of the mockup, but rather focus on the sequential nature of the requests in this timeline:
Or see this answer for an example of how to use Combine to perform requests concurrently, but constrain the degree of concurrency to something reasonable. Whenever possible, run requests concurrently because, otherwise, network latency effects will compound and make the overall process much slower.
Upvotes: 2