Using Swift 5.5 Async Await to Fetch REST API
Published at May 30, 2021
As an iOS developer, most probably you already have lot of experiences to fetch REST API from multiple endpoints. In some instances, the call to the API endpoints need to be in sequential order as they need the response of previous API call to be passed in HTTP Request.
Let me give you an illustation for such scenario with these given steps:
- Fetch IP address of the user from Ipify API
- Passing the IP address, fetch IP Geolocation data from FreeGeoIP API containing the country code.
- Passing the Country Code, fetch the Country Detail from RestCountries.eu API
If the final code we ended written will have 3 callbacks that are nested like the picture below, then welcome to Pyramid of Doom aka callback hell!
The code is not easily readable, complicated, and harder to maintain as the codebase growth. Let's say we need to use country detail response to fetch another API, we will add the 4th callback making the nesting become even much deeper.
The pyramid of doom callbacks problem has already been raised as one of the most requested issue that the Swift Language should eliminate. At 2017, Chris Lattner (Swift creator) had even written the Swift Concurrency Manifesto discussing his visions and goals to handle concurrency with imperative code using async await.
Finally with the accepted Swift Evolution Proposal (SE-0296), Async Await has been implemented in Swift 5.5 (Currently still in development as this article is written). The proposal provides the motivation to tackle 5 main problems:
- Pyramid of doom.
- Better error handling.
- Conditional execution of asynchronous function.
- Forgot or incorrectly call the callback.
- Eliminating design and performance issue of synchronous API because of callbacks API awkwardness.
Async Await provides the mechanism for us to run asynchronous and concurrent functions in a sequential way. This will help us to eliminate pyramid of doom callback hell and making our code easier to read, maintain, and scale. Take a look at the picture below showing how simple and elegant async await code is compared to using callback.
What We Will Learn and Build
In this article, we will be learning and experimenting Swift Async Await to solve these 2 tasks:
- Fetch Dependent multiple remote APIs sequentially. In this case, we are going to fetch IP Address from Ipify, geo location from FreeGeoIP, and country details from RestCountries.eu.
- Concurrently fetch multiple remote APIs in parallel. In this case, we are going to use SWAPI to fetch "Revenge of the Sith" data containing url of characters from the movie. Using these urls, we'll fetch all the characters data in parallel.
Here are 2 main topics that we will learn along the way:
- Task Based API introduced in Structured Concurrency proposal (SE-0304) as the basic unit of concurrency in async function. We'll be learning on how to create single task and task group async functions.
- Interfacing current synchronous code with completion handler callback to async tasks using Continuation API introduced in SE-0300. We'll be interfacing current URLSession data task callback based API to Async Await function.
Swift 5.5 is currently in development as this article is written, i am using Xcode 12.5 with Swift Nightly 5/29 Development Snapshot from swift.org. There might be an API changes that break the current code when Swift 5.5 stable is released in the future.
Getting Started
Please download and install the Swift 5.5 Xcode toolchain from this swift.org link. Then, open Xcode and select File > Toolchains > Swift > Swift 5.5 Development Snapshot 2021-05-28
.
Next, you need to clone or download the Starter Project from my GitHub Repository. It contains the starter code as well as the completed project.
As Async Await in Swift 5.5 development build currently doesn't have the support to run in iOS and macOS Cocoa GUI based App, the project is set as a macOS console based App. I have already added 3 additional flags in Package.swift
to help us experiment with async await:
"-Xfrontend", "-enable-experimental-concurrency"
. Enable the async await feature."-Xfrontend", "-disable-availability-checking"
. Eliminate the build error ofmacOS9999 availability checking
when using Task based API"-Xfrontend", "-parse-as-library"
. Eliminate the build error when declaring async main function so we can invoke async function inmain
.
Open the Starter folder and click on async-api.xcodeproj
to open the project in Xcode. The starter project already provides the Models that conform to Decodable when fetching the data from remote APIs:
- For
SWAPI
, we haveSWAPIResponse
,Film
, andPeople
structs. - For
GeoIP
, we haveIpifyResponse
,FreeGeoIPResponse
, andRestCountriesResponse
. - All the models provide static constant and method for the URL to fetch.
In the FetchAPITask.swift
file, i provided a global method to fetch remote API and decode the response data using URLSession data task and callback generic Result handler.
The entry-point of the App is in static main method in struct App inside the main.swift
file. It is declared using the @main
keyword introduced in Swift 5.3. Currently, it fetches the APIs using callbacks. You can try to build and run the app to see the results printed in the console, make sure you have internet connection before.
Let's move on to our first task, which is to create our own async function to fetch REST API concurrently using the all new Structured Concurrency Task based API
.
Create Fetch REST API Async Function using Task API
The async await proposal (SE-0296) itself doesn't introduce the mechanism to handle concurrency. There is another proposal called structured concurrency (SE-0304) which enables the concurrent execution of asynchronous code with Task based API model for better efficiency and predictability.
Based on the Task public interface, we can initialize a single task passing the optional TaskPriority
param and mandatory operation
param containing the closure with the return type. One of the initializer also support throwing error. For TaskPriority, we can pass several options such as none
, background
, default
, high
, low
, userInitiated
, utility
. If nil
is passed, system will use the current priority.
extension Task where Failure == Never {
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)
}
Open FetchAPITask.swift
file and type/copy the following code from the snippet.
// 1
func fetchAPI<D: Decodable>(url: URL) async throws -> D {
// 2
let task = Task { () -> D in
// 3
let data = try Data(contentsOf: url)
let decodedData = try JSONDecoder().decode(D.self, from: data)
return decodedData
}
// 4
return try await task.value
}
- We declare the
fetchAPI
function using ageneric D
type that conforms toDecodable
, it accepts a singleURL
parameter and returns the generic D type. Notice theasync
andthrows
keywords after the parameter. It is an async function that can throws.- We initialize a
Task
without passing theTaskPriority
and use trailing closure for theoperation
parameter. It has no parameter and return the generic D type. - The code inside the task closure runs in
async context
and another thread assigned by the system. Here, we fetch the data from the API using theData(contentsOf:)
throwing initializer which accepts anURL
. This is a blocking synchronous API used to fetch data and is bad for performance, but as we are running on another thread, it should be ok for now (later, we will learn on how to interface usingURLSession DataTask
). Then, we decode the response data usingJSONDecoder
to the generic D Type and return it. - Finally, we need to invoke task
value
property to wait for the task to complete and returning (or throwing) its result. This property isasync
, so we need to prefix the call usingawait
keyword. One thing to remember is we can only useawait
anasync
function in an async context. So, we cannot simply use this if our function is not anasync
function. For your info, there is anotherresult
property which returns theResult
type instead of the value.
- We initialize a
Next, let's go back to the main.swift
file and replace the current static main
code method with the following snippet.
// 1
static func main() async {
// 2
do {
// 3
let ipifyResponse: IpifyResponse = try await fetchAPI(url: IpifyResponse.url)
print("Resp: \(ipifyResponse)")
// 4
let freeGeoIpResponse: FreeGeoIPResponse = try await fetchAPI(url: FreeGeoIPResponse.url(ipAddress: ipifyResponse.ip))
print("Resp: \(freeGeoIpResponse)")
// 5
let restCountriesResponse: RestCountriesResponse = try await fetchAPI(url: RestCountriesResponse.url(countryCode: freeGeoIpResponse.countryCode))
print("Resp: \(restCountriesResponse)")
} catch {
// 6
print(error.localizedDescription)
}
}
- To make app entry-point runs in async context, we add the
async
keyword to the staticmain
method. Without this, we won't be able to invoke and await async functions. - As our fetchAPI async functions can throw, we need to use the standard do catch block.
- In the do block, first, we fetch the current IP Address of the user from
Ipify
API. We pass the url address from theIpifyResponse
struct static constant. We declare theipifyResponse
constant with the type ofIpifyResponse
so the fetchAPI method able to infer the D Generic placeholder asIpifyResponse
struct, we need to usetry await
as fetchAPI is an async function. - Next, we fetch the Geolocation data from FreeGeoIP API passing the IP address from the previous response. We declare
FreeGeoIPResponse
struct as the type that will be decoded by the fetchAPI. - Next, we fetch the Country data from RestCountry.eu API passing the country code from the FreeGeoIP response.
RestCountriesResponse
struct is the type of decodable data. - Finally, inside the catch block, we just print the error localizedDescription to the console. This will be invoked in case one of the API call fails.
That's it! try to run and build with active internet connection to see the responses printed in the console. We have successfully implement the Async function to fetch data from multiple REST API sequentially. You should be proud of the code that we write for this as it is very clean and readable.
Call Async Function in Synchronous Function using Detach
I have said before that we can't call async function in a synchronous function. Actually, there is another approach to do this using the detach
API.
static func main() {
detach {
do {
let ipifyResponse: IpifyResponse = try await fetchAPI(url: IpifyResponse.url)
//...
} catch {
print(error.localizedDescription)
}
}
RunLoop.main.run(until: Date.distantFuture)
}
Basically this task will run independently of the context which it is created. You might be wondering about this code RunLoop.main.run(until: Date.distantFuture)
. As this is a console based app with synchronous main function, the process will get terminated immediately as the function ends, using this, we can keep the process running until a specified date so the detach task can be executed.
Concurrently Fetch Multiple REST APIs in Parallel
Let's move on to the the second task, which is to fetch multiple APIs in parallel using async await. You might be wondering how can we achieve this as we don't want to sequentially fetch the APIs, it will be very slow and doesn't maximize the potential of our hardware such as M1 based SoC with 8 Cores (4x High Performance + 4x Efficiency)
The Task API from the Structured Concurrency proposal got you covered as it provides the GroupTask
API to execute group of async functions in parallel.
Using the GroupTask we can spawn new async child tasks within the async scope/closure. We need to complete all the child tasks inside the scope before it exits.
Navigate to the FetchAPITask.swift
file and type/copy the following code snippet.
// 1
func fetchAPIGroup<D: Decodable>(urls: [URL]) async throws -> [D] {
// 2
try await withThrowingTaskGroup(of: D.self) { (group) in
// 3
for url in urls {
group.async {
let data = try Data(contentsOf: url)
let decodedData = try JSONDecoder().decode(D.self, from: data)
return decodedData
}
}
// 4
var results = [D]()
for try await result in group {
results.append(result)
}
return results
}
}
- We declare the
fetchAPIGroup
function that usegeneric D type
that conforms toDecodable
. The parameter accepts an array ofURL
, it means we can havedynamic number
of URLs. The return type is an array of D generic type. We declare theasync
andthrows
keywords after the parameter to make this an async function that can throw. - We use the
withThrowingTaskGroup
API passing the return type of D and the closure with group as the single parameter without return type. We need to invoke this code usingtry await
as it is an async function. - In the closure, we use for-loop in the url array. In each loop, we use the group async method to spawn a new async child task. In the closure, we just fetch the data using
Data(contentsOf:)
initializer, decode the data as D type, and return. You might be thinking why don't we callfetchAPI
instead, please don't do this as the app will crash. I am not really sure why, please tell me the reason if you know the reason. - We declare a property containing array of D type. Here, we use the
AsyncSequence
to iterate and use try await at the group child tasks, the child task that finish first is appended to results array. Finally, we return the array after all the child tasks has been completed. TaskGroup itself implementsAsyncIterator
so it has thenext()
method for iteration, you can take a look at the Async Sequence proposal (SE-0298) to learn more about the detail.
One thing to consider when using withThrowingTaskGroup is if one of the child task threw an error before the closure completes, then the remaining child tasks will be cancelled and error will be thrown
. If we want one of the child fails without making the remaining tasks cancelled, we can use withTaskGroup
instead and return optional value instead of throwing. Next, navigate back to the main.swift
file and replace the static main
method with the following snippet.
// 1
static func main() async {
do {
// 2
let revengeOfSith: SWAPIResponse<Film> = try await fetchAPI(url: Film.url(id: "6"))
print("Resp: \(revengeOfSith.response)")
// 3
let urlsToFetch = Array(revengeOfSith.response.characterURLs.prefix(upTo: 3))
let revengeOfSithCharacters: [SWAPIResponse<People>] = try await fetchAPIGroup(urls: urlsToFetch)
print("Resp: \(revengeOfSithCharacters)")
} catch {
// 4
print(error.localizedDescription)
}
}
- The
main
static method is declared withasync
method to enable async context. - In the do block, first we fetch the "Revenge of the Sith" with using the
fetchAPI
async function, the URL is retrieved from theFilm
struct static url method passing id of6
. (6 is the film id of Revenge of the Sith in SWAPI). The Film response provided by SWAPI contains an array of the characters URLs. - We'll fetch the first 3 characters in the array using the
fetchAPIGroup
async function passing the film'scharacterURLs
property, we slice the array using prefix method to get the first 3 elements. The return type of this is Array ofSWAPIResponse<People>
. - In the catch block, we just print the error localizedDescription property.
That's it, make sure to run and build with internet connection to see the responses printed in the console. We have successfully implemented async function to group child tasks so it can be executed in parallel concurrently.
One more Thing - Interfacing Current Synchronous Code with Callback to Async Function (URlSession DataTask)
Before we conclude this article, there is one more thing that we need to learn, the Continuation API
. This API is super important as we can use it to convert our current synchronous code with callback to async function. As an example for this article, we'll convert the URLSession DataTask
as an async function.
Create a new Swift file named URLSession+Async.swift
, we'll create the async method as an extension
URLSession. Type/copy the following code snippet into the file.
import Foundation
extension URLSession {
// 1
func data(with url: URL) async throws -> Data {
// 2
try await withCheckedThrowingContinuation { continuation in
// 3
dataTask(with: url) { data, _, error in
// 4
if let error = error {
continuation.resume(throwing: error)
} else if let data = data {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Bad Response"]))
}
}
.resume()
}
}
}
- Declare the
data
method inside URLSession extension. It provides a single parameter that accepts URL. The return type isData
. Theasync
andthrow
is declared to make this a throwing async method. - Invoke
try await
onwithCheckedThrowingContinuation
function. Here we need to pass the closure with a single parametercontinuation
. Basically, we need to invoke the resume method passing the returning data or the throwing error. Make sure to invoke theresume
methodexactly once to avoid undefined behavior
.withCheckedThrowingContinuation
will crash if we invoke it twice, but it has runtime performance cost. Swift also provideswithUnsafeThrowingContinuation
to avoid this. In this case, the system won't crash if we invoke resume twice and can lead to undefined behavior! - Invoke the URLSession dataTask passing the url and completion closure. Make sure to invoke resume on the data task.
- In the closure, we check if the error exists, we invoke the continuation resume passing the throwing error. If data exists, we invoke the continuation resume passing the returning data. Else, we invoke the continuation resume passing our own constructed throwing error. For simplicity of this article, i don't check the HTTP Response status code, but you must do this in production.
That's it! now we can fetch our REST API using URLSession
and remove the Data(contentsOf:)
blocking API. Let's navigate back to FetchAPITask.swift
file and type/copy the following snippet.
func fetchAndDecode<D: Decodable>(url: URL) async throws -> D {
let data = try await URLSession.shared.data(with: url)
let decodedData = try JSONDecoder().decode(D.self, from: data)
return decodedData
}
The fetchAndDecode
async function basically fetches the data using shared URLSession data(with:)
async method we created previously. Then, it decode data using the generic placeholder and return the decoded model.
Now, let's replace the fetchAPI
and fetchGroupAPI
to use this new function to fetch and decode data.
func fetchAPI<D: Decodable>(url: URL) async throws -> D {
let task = Task { () -> D in
try await fetchAndDecode(url: url)
}
//...
}
func fetchAPIGroup<D: Decodable>(urls: [URL]) async throws -> [D] {
try await withThrowingTaskGroup(of: D.self) { (group) in
for url in urls {
group.async { try await fetchAndDecode(url: url) }
}
//...
}
}
That's all! now try to build and run to make sure everything work just like before. You can check the completed project from the repository link above.
What's Next
As async await itself is a pretty big API with so many features, i won't be able to cover everything in single article. So please read the proposals themselves to learn the things i haven't covered in this article such as:
- Task Cancellation.
- Task Yield.
- Async Let binding.
I also recommend you to read Hacking with Swift by Paul Hudson - What's new in Swift 5.5. That one article, is also one of my source and inspiration to write this article
Also, so far, i haven't discussed on how to handle race condition and data isolation between different threads. There is another proposal named Actors (SE-0306) that solves this problem in case you are interested to learn more. I believe it is a very important concept to understand and implement so we can produce much stable code in production without race condition bugs.
Conclusion
Congratulations on making it so far reading this long article. Before i conclude the article, i want to give my own points on the async await API:
- Async Await helps us to manage code complexity and complication when using many functions with callback, as well as providing simpler control flow and error handling.
- Very useful for UI and Server Domain where everything should be asynchronous and non-blocking
- Combine is an alternative approach for managing asynchronous flow using streams in a reactive way using Publisher and Subscribers. It is a closed source framework and not built into Swift, and can only be used in Apple Platforms.
It is such an amazing time to be a part of Swift developers community and build wonderful things to solve complex challenging problems using technology. So, let's keep on becoming a lifetime learner and coder!