Update for iOS 15+ (Swift 5.5)
I've added a more modern solution for Swift 5.5 and iOS 15+ because this toolchain includes major URLSession
API improvements, that are not specific to Firebase or Alamofire. The code uses async / await
i.e. Structured Concurrency
. It's what Apple recommends for concurrent requests on the latest iOS versions (iOS 13.0+).
We now achieve the same result as DispatchGroup
s with fewer lines of code and more customisation. This answer will help users who used to queue URLSession
requests and wait for these to complete.
Task group example code
The right tool is a TaskGroup
if we have a dynamic number of requests (variable-sized array).
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.addTask {
return (id, try await fetchOneThumbnail(withID: id))
}
}
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
}
func fetchOneThumbnail(withID id: String) async throws -> UIImage {
// Just for demo purpose. In PROD, we may use dynamic URLs for each ID.
guard let url = URL(string: "http://placekitten.com/200/300") else {
throw ThumbnailError.invalidURL
}
// I have used `data(from: URL, delegate: URLSessionTaskDelegate? = nil)`
// but we can also use `data(for: URLRequest, delegate: URLSessionTaskDelegate? = nil)`)`.
// If we want to observe the delegate changes like when the
// request fails, completes, or redirects, use the delegate param.
// e.g. try await URLSession.shared.data(from: url, delegate: delegate)
let result: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: result.data) else {
throw ThumbnailError.missingImageData
}
return image
}
enum ThumbnailError: Error {
case invalidURL
case missingImageData
}
Task {
let images = try await fetchThumbnails(for: ["1", "2", "3"])
// Show thumbnails in UI.
}
This also uses the for await
loop (AsyncSequence
) to wait for tasks to complete. for try await
is an example of a throwing AsyncSequence
. The throwing syntax is because the new asynchronous URLSession.data(for:)
family of methods are throwing functions.
async let
example code
async let
syntax works for a fixed number of requests.
let reqOne = urlRequest(for: keyOne) // Function that returns a unique URLRequest object for this key. i.e. different URLs or format.
async let (dataOne, _) = URLSession.shared.data(for: reqOne)
let reqTwo = urlRequest(for: keyTwo)
async let (dataTwo, _) = URLSession.shared.data(for: reqTwo)
guard let parsedData = parseInformation(from: try? await dataOne) else {
// Call function to parse image, text or content from data.
continue
}
// Act on parsed data if needed.
guard let parsedDataTwo = parseInformation(from: try? await dataTwo) else {
// Call function to parse image, text or content from data.
continue
}
// Act on the second requests parsed data if needed.
// Here, we know that the queued requests have all completed.
The syntax where I don't await
for the request to finish immediately is called async let
.
This code example could be adapted with variable-sized arrays but isn't recommended by Apple. This is because async let
doesn't always allow the requests to be processed as soon as they arrive.
The benefits of this approach are cleaner code that's easier to write, safer, and avoiding deadlocks/threading issues.
Note
The exact syntax of TaskGroup
and async let
may change in the future. Currently, Structured Concurrency has improved a lot during its early releases and is now stable for production.
Apple has clarified that the underlying mechanics of grouped and asynchronous tasks are mostly finalised (approved in Swift Evolution). An example of some syntax changes already includes the replacement of async {
with Task {
.