Concurrency Demystified

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

Lesson 01: The Power of Async/Await

Asynchronous Programming Demo

Episode complete

Play next episode

Next
Transcript

In this lesson, we’ll go over the traditional asynchronous code and see how error prone and difficult it is to maintain.

Explaining the Code in NewsService

Open the file NewsService.swift, and look at the function latestNews(:).

This function utilizes asynchronous programming to fetch news articles from a remote server, handling errors and parsing the response accordingly.

func latestNews(_ handler: @escaping (Result<[Article], NewsServiceError>) -> Void)

The function takes a completion handler receiving a Result type containing either an array of Article or an error of type NewsServiceError.

Here’s a breakdown of the code.

The function starts by defining a URLSession data task. This task is responsible for making a network request to a specified URL to retrieve data asynchronously:

let task = URLSession.shared.dataTask(with: URLRequest(url: Self.newsURL)) { data, response, error in
  ...
}

After defining the data task, the function immediately starts it by calling task.resume(). This call initiates the network request:

task.resume()

The completion handler of the data task is a closure that takes three parameters: data, response, and error.

This closure is executed when the network request completes, whether successfully or with an error.

If there’s an error during the network request (such as no internet connection), it logs the error using a logger and calls the completion handler with a .failure result containing a NewsServiceError.networkError:

if let error {
  Logger.main.error("Network request failed with: \(error.localizedDescription)")
  handler(.failure(.networkError))
  return
}

If the response from the server is not of type HTTPURLResponse, it logs an error and calls the completion handler with a .failure result containing a NewsServiceError.serverResponseError:

guard let httpResponse = response as? HTTPURLResponse else {
  Logger.main.error("Server response not HTTPURLResponse")
  handler(.failure(.serverResponseError))
  return
}

If the HTTP response status code is not in the 200-299 range (indicating success), it logs an error and calls the completion handler with a .failure result containing a NewsServiceError.serverResponseError:

guard httpResponse.isOK else {
  Logger.main.error("Server response: \(httpResponse.statusCode)")
  handler(.failure(.serverResponseError))
  return
}

If the received data is nil, it logs an error and calls the completion handler with a .failure result containing a NewsServiceError.serverResponseError:

guard let data else {
  Logger.main.error("Received data is nil!")
  handler(.failure(.serverResponseError))
  return
}

If the data is successfully received, it attempts to decode the JSON response into a Response object using JSONDecoder:

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

  handler(.success(apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }))
} catch {
  Logger.main.error("Response parsing failed with: \(error.localizedDescription)")
  handler(.failure(.resultParsingError))
}

If the decoding fails, it logs an error and calls the completion handler with a .failure result containing a NewsServiceError.resultParsingError.

Finally, if the decoding succeeds, it filters the articles in the response to remove those where either the author or the URL to the image is nil.

It then calls the completion handler with a .success result containing the filtered articles.

Debugging the Code

To see how the code execution flows, put a breakpoint on the following lines.

The first one on the task definition:

let task = URLSession.shared.dataTask(with: URLRequest(url: Self.newsURL)) { data, response, error in

The second one on the first line of the completion handler:

if let error {

The third one on the resume task at the end of the function:

task.resume()

Start debugging the code by clicking the Run button or by selecting Run in the Product menu.

Once the app is launched in the simulator, tap Load Latest News. The first breakpoint is hit, and the execution pauses. In the Debugger navigator pane, you can see that the function is executed on the main thread.

Now, click Continue Program Execution. The execution continues, and the third breakpoint is hit.

The execution is still on the main thread.

Click Continue Program Execution again. After some time, once the server responded to the network request, the execution goes back to the second breakpoint.

You can now see that the execution is on a secondary thread.

Click Continue Program Execution again. The app presents the news articles.

Note that the caller of latestNews(:), the function fetchLatestNews() in NewsViewModel, is responsible for parsing the result and updating the news variable on the main thread:

func fetchLatestNews() {
  news.removeAll()
  newsService.latestNews { [weak self] result in
    DispatchQueue.main.async {
      switch result {
      case .success(let articles):
        self?.news = articles
      case .failure:
        self?.news = []
      }
    }
  }
}

From the debug session, you can see the complexity of following the execution with the completion handler.

The execution flow is not linear, and the code is error-prone.

No Help From the Compiler

As an example of how easy it is to compromise the code functionality, comment out the final call of the completion handler in the function latestNews(:) in NewsService:

//handler(.success(apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }))

Turn off the debugger, and rerun the program.

Once the app is launched in the simulator, tap Load Latest News.

Nothing happens! You have no errors and no results.

This is a pretty straightforward case with a prominent error. But think about how difficult it is to spot errors like this when you have nested completion call handlers and forget to call the completion handler in one of the rare error cases.

Async/await will streamline the code execution flow and help you manage these subtle errors by checking at compile time that your code always returns a result.

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