NewsService
to replace concurrency based on the completion
handler with async/await.
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]
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.
async
indicates that the function can
be suspended during its execution, unblocking the CPU cores to run other code.
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 }
}
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.
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.
URLSession
throws an error
that will be propagated in the error response chain.
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
}
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 }
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"
)
]
}
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
NewsViewModel.swift
.
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:
-
TheTask
instruction allows asynchronous code to run within a synchronous context, as the functionfetchLatestNews()
. -
SincelatestNews
is now an asynchronous function, you need to use the keywordawait
(andtry
) when calling it. -
Thenews
variable triggers UI updates, so it needs to be updated on the main thread.
Task
closure is executed asynchronously,
but by default, it inherits the execution context of the caller.
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.