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:
-
The
Task
instruction allows asynchronous code to run within a synchronous context, as the functionfetchLatestNews()
. -
Since
latestNews
is now an asynchronous function, you need to use the keywordawait
(andtry
) when calling it. -
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.