Asynchronous programming has become a cornerstone of modern app development. In the iOS ecosystem, Apple introduced native support for asynchronous programming with Swift, including the powerful async/await pattern. In this article, we'll delve into the usage of async/await in iOS development
Understanding async/await
Async/await is a programming paradigm that streamlines the execution of asynchronous code, making it appear more synchronous and readable. It eliminates the complexities of managing callbacks or completion handlers, allowing developers to write asynchronous code that same as synchronous code. With Swift's async/await, developers can efficiently handle tasks such as network requests, database queries, or file I/O without blocking the main thread
Basic Usage of async/await in iOS:
-
Define an async Function: To create an asynchronous function, simply use the
async
keyword in the function declaration. This signifies that the function will execute asynchronous tasks.
func fetchData() async -> Data { // async operations }
-
Use await Inside an async Function: Within an async function, utilize the
await
keyword to call other asynchronous functions or tasks. It suspends the current task until the awaited task is completed.
func fetchImageData() async -> Data { let url = URL(string: "https://example.com/image.jpg")! do { let (data, _) = try await URLSession.shared.data(from: url) return data } catch { // Handle errors } }
-
Mark the Caller as async: When calling an async function from another function, mark the calling function as async as well. This facilitates the use of
await
for calling async functions.
func displayImage() async { let imageData = await fetchImageData() let image = UIImage(data: imageData) // Update UI with the image }
-
Error Handling: Error handling with async/await is straightforward. Utilize
try
andcatch
blocks to handle errors thrown within async functions.
func fetchImageDataWithThrowing(stringURL: URL) async throws -> Data { guard let url = URL(string: stringURL) else { throw NSError(domain: "Bad URL", code: 0) } let (data, _) = try await URLSession.shared.data(from: url) return data } func displayData() async { let url = "https://example.com/image.jpg" do { let data = try await fetchImageDataWithThrowing(stringURL: url) // Process data } catch { // Handle errors } }
-
Running async Code on the Main Thread: To update the UI on the main thread within async code, use
await
to dispatch code to the main queue.
Task { await MainActor.run { // Update UI } }
or
Task { @MainActor in // Update UI }
Advanced Use Cases:
-
Calling an Asynchronous Function in Synchronous Code: Utilize the
Task
structure to denote the scope of an asynchronous call within synchronous code.
func upadeImg() { Task { let image = await fetchImageData() MainActor.run { // Update UI } } }
-
Converting a Function with a Callback to Asynchronous: To create an asynchronous function, simply use the construct like
withCheckedContinuation
orwithCheckedThrowingContinuation
. This may help you if you want to use async/await at the network layer with iOS before version 15 and after 13.
extension URLSession { func asyncDataRequest(url: URL) async throws -> Data { try await withCheckedThrowingContinuation { next in dataTask(with: url, completionHandler: { data, response, error in if let error { next.resume(throwing: error) } else if let data { next.resume(returning: data) } else { next.resume(throwing: NSError(domain: "Empty data", code: 0)) } }).resume() } } }
-
Running Multiple Queries in Parallel: Use constructs like
async let
orwithTaskGroup
orwithThrowingTaskGroup
to execute multiple asynchronous methods concurrently, reducing lines of code and improving efficiency.
func fetchUser() async -> User { async let imageData = await fetchImageData() async let fullName = await fetchUserFullName() return await User(avatar: imageData, fullName: fullName) }
or
withTaskGroup
func fetchImages(imageID: [String]) async -> [Data] { await withTaskGroup(of: Data.self) { group in imageID.forEach { id in group.addTask { await fetchImageData(byID: id) } } return await group.reduce(into: [Data]()) { tmpResult, imageData in tmpResult.append(imageData) } } }
Conclusion
Async/await in Swift presents a powerful and readable approach to asynchronous programming in iOS applications. By following the outlined steps, developers can simplify their asynchronous code, improve app responsiveness, and leverage Swift's robust asynchronous programming capabilities to create more efficient and responsive iOS apps