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.

Weather API Setup
Weather API Setup

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:

  1. apiKey correlates to the WeatherAPI API key you created earlier. Copy that API key and replace "<INSERT_API_KEY">.
  2. baseUrl, currentWeatherPath, queryName, and keyParamName are all private variables. You use these for building the URL in step 3. For further details, check out the API Swagger doc.
  3. This getWeather() implementation builds a URL that you can then fire off an API request for. Notice here is where you use WeatherServiceError.invalidURL. While unlikely, if you ever change the variables in step 2, it can be a good indication something went wrong.
  4. You await on URLSession to get the data from the API. Once retrieved, you decode and return the data. In this case, you’re returning a WeatherData object. WeatherData already conforms to the Decodable protocol. Check out the WeatherAppSample/Data/Models/ folder for the Swift files containing WeatherData and 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 with Result types 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:

  1. You use the weatherDataList first as a caching layer. If there’s a hit based on the query, return that instead of needlessly making an API call.
  2. If the cache hit fails, then fetch the data from the API.
  3. Add the fetched weatherData to the cache.
  4. 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)
    }
  }
}
  1. 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.
  2. Start a task and call into the repo’s fetchWeather function, 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.

See forum comments
Download course materials from Github
Previous: Introduction Next: Conclusion