Concurrency Demystified

Jun 20 2024 · Swift 5.10, iOS 17, Xcode 15.3

Lesson 01: The Power of Async/Await

Refactoring With Async/Await Demo

Episode complete

Play next episode

Next
Transcript

Now, you’ll refactor NewsService to replace concurrency based on the completion handler with async/await.

In NewsService.swift, start by defining a new protocol function to load the news with async/await.

protocol NewsService {
  ...
  func latestNews() async throws -> [Article]
}

The first difference to the previous definition is already in the function signature:

func latestNews() async throws -> [Article]

Instead of taking a completion handler that will receive a Result that needs to be resolved, the function simply returns an array of Article objects.

If there’s an error during the processing, the function throws an error.

Last but not least, the keyword async indicates that the function can be suspended during its execution, unblocking the CPU cores to run other code.

You should now see an error indicating that the NewsAPIService does not conform to the NewsService protocol. Lets fix this by implementing the new function:

func latestNews() async throws -> [Article] {
  // 1. Async network request
  let (data, response) = try await URLSession.shared.data(from: Self.newsURL)

  // 2. Response parsing
  guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK else {
    Logger.main.error("Network response error")
    throw NewsServiceError.serverResponseError
  }

  // 3. Response decoding
  let apiResponse = try JSONDecoder().decode(Response.self, from: data)
  Logger.main.info("Response status: \(apiResponse.status)")
  Logger.main.info("Total results: \(apiResponse.totalResults)")

  // 4. Filtering
  return apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }
}

Analyzing the Function’s Implementation

The first line uses URLSession.shared.data(from:) to perform an asynchronous network request:

let (data, response) = try await URLSession.shared.data(from: Self.newsURL)

This method fetches the contents of the specified URL as a tuple containing the retrieved data and the URL response.

The await keyword is used to suspend the function’s execution until the asynchronous operation completes. This allows the function to wait for the network request to finish without blocking the calling (main) thread.

If there’s any error in network operation, URLSession throws an error that will be propagated in the error response chain.

After the network request is completed, the function checks if the response is an HTTPURLResponse and its status code indicates success (200). If not, the function throws a NewsServiceError.serverResponseError:

guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK else {
  Logger.main.error("Network response error")
  throw NewsServiceError.serverResponseError
}

If the response is valid, the function attempts to decode the received data into an array of Article using JSONDecoder(). If decoding fails, it throws an error:

let apiResponse = try JSONDecoder().decode(Response.self, from: data)
Logger.main.info("Response status: \(apiResponse.status)")
Logger.main.info("Total results: \(apiResponse.totalResults)")

Finally, the results are filtered and returned:

return apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }

Before we continue, there is one more error we need to take care of. Notice, that the MockNewsService does not conform to the NewsService protocol. Lets fix this by adding the following function to the MockNewsService class:

func latestNews() async throws -> [Article] {
  return [
    Article(
      title: "Lorem Ipsum",
      url: URL(string: "https://apple.com"),
      author: "Author",
      description:
      """
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
      incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam...
      """,
      urlToImage: "https://picsum.photos/300"
    )
  ]
}

Lets go back to the latestNews implementation of NewsAPIService. Notice that the execution flow is now linear, from the top to the bottom of the function. No more going back and forth, as in the previous case.

Furthermore, the compiler now enforces that the function will either return a value or throw an error.

Try to comment out the line where the function throws the error:

//throw NewsServiceError.serverResponseError

You’ll see that the compiler complains about the error:

'guard' body must not fall through, consider using a 'return' or 'throw'
to exit the scope

Adapting the Caller Side

Open NewsViewModel.swift.

Adapt the function fetchLatestNews with the following code:

func fetchLatestNews() {
  news.removeAll()

  // 1. Use `Task` to run asynchronous code in a synchronous context
  Task {
    // 2. Need `try await` because the function is `async throws`
    let news = try await newsService.latestNews()
    // 3
    self.news = news
  }
}

Here’s what’s happening in the code:

  1. The Task instruction allows asynchronous code to run within a synchronous context, as the function fetchLatestNews().

  2. Since latestNews is now an asynchronous function, you need to use the keyword await (and try) when calling it.

  3. The news variable triggers UI updates, so it needs to be updated on the main thread.

In this case, the code inside the Task closure is executed asynchronously, but by default, it inherits the execution context of the caller.

Since it’s called from the main thread, any updates to self.news inside the Task closure will also happen on the main thread.

Now go ahead and run the app on the simulator. You should see that the news app should work just fine with our refactoring.

See forum comments
Cinema mode Download course materials from Github
Previous: Discovering Async/Await Next: Conclusion