Instruction
Eureka! You have an amazing idea and you know it’s going to be a success. You start by creating a new project in Xcode and get your UI built out. Then, it’s time for making an API request… so, what’s the best way to do that? After all, there are many ways to make an API request and retrieve the result.
The simplest approach is Swift’s new async/await structured concurrency. It provides a succinct and coherent code style for all your asynchronous needs.
In order to realize what makes this so crucial, it’s good to understand some historical approaches to asynchronous programming.
Historical Asynchronous Approaches
Before async/await, asynchronous programming relied on older paradigms and antiquated patterns.
Here are some of the most common approaches:
Completion Handlers
One of the most common methods used was completion handlers. Completion handlers allowed you to pass in a parameter called a closure:
func fetchData(completion: @escaping (Data) -> Void) {
let data = ... // Fetch data
... // Do other stuff
completion(data)
}
}
In the example above, completion is a closure that represents a function as a parameter. When you call completion at the end of fetchData(), it performs a callback to the call site.
The call site code could look something like this:
fetchData { data in
print("Data: \(data)")
}
While this approach works, you can see how it’s also problematic — if you have many functions with completion handlers, the code can become confusing. Also, if you have nested handlers, AKA “callback hell”, then your code becomes incoherent.
Grand Central Dispatch
Part of Grand Central Dispatch includes DispatchQueue. You could write the following code:
DispatchQueue.global().async {
let data = ... // Fetch data
... // Do other stuff
DispatchQueue.main.async {
print("Data: \(data)")
}
}
Like completion handlers, one immediate downside to this approach is callback hell! Another downside is that it requires you to manage the queues; you have to state which queues to run on and at which points in the code.
There are other asynchronous approaches, but these two are the most common.
Now, you’ll take a look at the power behind Swift’s async/await.
Understanding the Basics of Async/Await
Marking a function with async indicates that the function performs asynchronous work. To call an async function, you use the await keyword.
await will suspend execution until the asynchronous task completes. During suspension, the thread can handle other tasks — this makes the app more efficient and responsive.
Take a look at the following code:
func fetchData() async -> String {
await Task.sleep(2_000_000_000) // Simulates a delay of 2 seconds
return "Data received"
}
Here, you notated fetchData() with async. The only ways to call this function is either with await, or from within another async function. In the body of this function, you’re able to await on Task.sleep(). Once the sleep function has completed, you then return the string "Data received".
Now, to call fetchData(), you would need to reference it from another async function. One way to do this is to use a Task:
Task {
let data = await fetchData()
print(data)
}
Instantiating a Task with an async closure allows the code to suspend while awaiting on any asynchronous tasks the closure may contain.
You can’t await on the main thread, as you’ll end up with a compiler error:
'async' property access in a function that does not support concurrency
Now that you know what async/await is, you’re ready to put it to use!
Creating a Weather API Account
To show async/await in action, you’ll create an application that fetches weather data. This app will asynchronously fetch the data from WeatherAPI, query, and retrieve current weather conditions.
To begin, create an account for the WeatherAPI here: https://www.weatherapi.com/signup.aspx.
The service itself offers a free tier plan, including the basic access required for this course’s lessons.
Once you sign up, go to your API dashboard and copy the API key — you’ll need this later on.
Now that you have your API key, you’re ready to begin making queries.
Setting Up the Weather Service
Open the starter project and go to the WeatherAppSample/Network/ folder. Open up WeatherService.swift. This file contains the following protocol:
protocol WeatherService {
func getWeather(for query: String) async throws -> WeatherData
}
The service requires a method getWeather(for:) to fetch weather data for a specified query. The query can be something like a city name or zip code. It returns a WeatherData type. WeatherData is a struct and resides under the WeatherAppSample/Data/Models/ folder.
There’s also a file called WeatherServiceError.swift:
enum WeatherServiceError: Error {
case invalidURL
}
This is a custom error subclass that you’ll need later in the service implementation.
Now, open WAPIWeatherService.swift. Replace the file contents with the following:
class WAPIWeatherService: WeatherService {
// 1
private let apiKey = "<INSERT_API_KEY>"
// 2
private let baseUrl = "https://api.weatherapi.com/v1"
private let currentWeatherPath = "/current.json"
private let queryParamName = "q"
private let keyParamName = "key"
// 3
func getWeather(for query: String) async throws -> WeatherData {
var urlComponents = URLComponents(string: baseUrl + currentWeatherPath)
urlComponents?.queryItems = [
URLQueryItem(name: queryParamName, value: query),
URLQueryItem(name: keyParamName, value: apiKey),
]
guard let url = urlComponents?.url else {
throw WeatherServiceError.invalidURL
}
// 4
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
return try decoder.decode(WeatherData.self, from: data)
}
}
Let’s break this down a bit:
-
apiKeycorrelates to the WeatherAPI API key you created earlier. Copy that API key and replace"<INSERT_API_KEY">. -
baseUrl,currentWeatherPath,queryName, andkeyParamNameare all private variables. You use these for building the URL in step 3. For further details, check out the API Swagger doc. - This
getWeather()implementation builds a URL that you can then fire off an API request for. Notice here is where you useWeatherServiceError.invalidURL. While unlikely, if you ever change the variables in step 2, it can be a good indication something went wrong. - You
awaitonURLSessionto get the data from the API. Once retrieved, you decode and return the data. In this case, you’re returning aWeatherDataobject.WeatherDataalready conforms to theDecodableprotocol. Check out the WeatherAppSample/Data/Models/ folder for the Swift files containingWeatherDataand subsequent structs.
Note: You may have realized that you did a
try await. This lesson purposefully does not cover any special error handling — error handling withResulttypes is covered in a later lesson.
Wiring Up the Repository
Next, you need to call the WeatherServiceImpl from the repository layer. Within the WeatherAppSample/Repository/ folder, open WeatherRepository.swift and note the protocol:
protocol WeatherRepository {
func fetchWeather(for query: String) async throws -> WeatherData
}
The protocol contains a single function for fetching the weather. The function is asynchronous and can throw an error if it encounters an error. It returns a WeatherData type.
Now, open up WeatherRepositoryImpl.swift and replace the fetchWeather function with the following:
func fetchWeather(for query: String) async throws -> WeatherData {
// 1
if let weatherData = weatherDataList[query] {
return weatherData
}
// 2
let weatherData = try await weatherService.getWeather(for: query)
// 3
weatherDataList[query] = weatherData
// 4
return weatherData
}
Here’s what’s happening:
- You use the
weatherDataListfirst as a caching layer. If there’s a hit based on the query, return that instead of needlessly making an API call. - If the cache hit fails, then fetch the data from the API.
-
Add the fetched
weatherDatato the cache. -
Return the
weatherData.
Setting Up the Presentation
This last part is to wire up the repository to the presentation layer. Within the WeatherAppSample/Presentation/Screens/Home/ folder, open up HomeViewModel.swift. In it, add a helper function to update the UI state:
func updateState(state: HomeState) {
DispatchQueue.main.async {
self.state = state
}
}
Updating the UI state is done on the main thread using DispatchQueue. In the next lessons, we’ll learn new ways to run code on the main thread asynchronously.
Finally, replace the getWeather function with the following:
func getWeather(query: String) {
// 1
state = .loading
// 2
Task {
do {
let weatherData = try await weatherRepo.fetchWeather(for: query)
updateState(state: .ready(weatherData))
} catch (_) {
updateState(state: .error)
}
}
}
- Here you are setting the state to
.loading. The UI is already wired up in HomeScreen.swift if you want to check out what each of the state changes do. - Start a task and call into the repo’s
fetchWeatherfunction, then await its response. Depending on the result, or error, update the state.
That’s it! Now you’re ready to test out the full async/await implementation.
Testing the Implementation
Run the app to ensure that the weather data fetches and displays.
You can use different city names to test various scenarios.
Keep in mind, too, that the app caches queries — in a given app session, cached queries should immediately return without a new API call.