I'm beginner and async/await
topic makes more so confused and can't really figure out what is the benefit of using it over URLSession
for example I have 3 set of data that I need to fetch them, currently I call the first one, and when it's done, I add the second one on its completion
.
public func example1() {
NetworkingModels.shared.fetchFirstSet(completion: { events in
switch events {
case .success:
example2()
completion(.success(()))
case .error(let error):
Logger.logError(error)
}
})
}
public func example2() {
NetworkingModels.shared.fetchSecondSet(completion: { events in
switch events {
case .success:
example3()
completion(.success(()))
case .error(let error):
Logger.logError(error)
}
})
}
public func example3() {
NetworkingModels.shared.fetchThirdSet(completion: { events in
switch events {
case .success:
completion(.success(()))
case .error(let error):
Logger.logError(error)
}
})
}
And if I make the function async and use new async/await
. call them like that
Task {
await example1()
await example2()
await example3()
}
What would be the benefit (except the cleaner code)?
and for example, here I have a function to get the html of an URL, it's written with URLSession
public func getHTML(url: String, completion: @escaping Result<String>.Completion) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let sessionConfiguration = URLSessionConfiguration.default
if let userAgent = Settings.Generic.userAgent {
sessionConfiguration.httpAdditionalHeaders = ["User-Agent": userAgent]
}
let session = URLSession.init(configuration: sessionConfiguration)
session.dataTask(with: request) {data, response, error in
if error != nil {
DispatchQueue.main.async {
completion(.error(HTMLError.failedToExtractHTML))
}
}
if let data = data {
if let html = String(data: data, encoding: .utf8) {
completion(.success((html)))
}
}
}.resume()
}
So if I change it to be sync/await
it makes the process faster? more reliable? What is the benefit here?
I would be grateful if someone can make it more clear to me. Thanks
As others have pointed out:
The question is not async
-await
vs. URLSession
, but rather, when using URLSession
, do you want to use the old completion handler pattern (or even older delegate pattern) or do you want to use the newer Swift concurrency with its async
-await
.
We do not adopt Swift concurrency and its async
-await
for performance reasons. (It can be marginally more efficient for reasons beyond the scope of this question, but it generally is not material.) We do it because it lets us write better code more easily:
For example, in getHTML
, did you notice that you have multiple paths of execution (if the URL was invalid; if the String
conversion failed) where you completely fail to call the completion handler? With Swift concurrency, the compiler would warn you of that error.
In completion block patterns, the code in the block will generally be called later than code later in the function. E.g., the resume
function is called before the code in the completion block. In this case, it is not too hard to reason about, but in more complicated scenarios, it becomes hard to follow. Swift concurrency eliminates that.
In completion block patterns, managing dependencies between multiple tasks that are, themselves, asynchronous, quickly becomes unwieldy. You can end up with completion blocks inside completion blocks, which can become quite hard to grok. To address this, we would often resort to other patterns (Operation
objects, futures/promise libraries, Combine, etc.), many of which are quite complicated, themselves. Swift concurrency eliminates all of that.
In completion block patterns, there is usually a complicated “completion” closure parameter (which you have apparently omitted from your example1
, example2
, etc., code snippets) with its own Result
parameter. In async
-await
patterns, you simply return
values and throw
errors. A lot of the syntactic noise just disappears.
Note, in the old GCD completion handler pattern, we often manually dispatch completion handlers back to the main queue. (In getHTML
, you are doing this for errors but curiously not for successes.) In Swift concurrency, you simply mark methods or classes that have to happen on the main actor (e.g., with @MainActor
). That almost always eliminates the need for the called function to manually “dispatch” stuff back to the main queue/actor. The compiler can take care of much of this for you.
For what it is worth, here is a async
-await
rendition of getHTML
:
public func getHTML(url: String) async throws -> String {
guard let url = URL(string: url) else { throw URLError(.badURL) }
let sessionConfiguration = URLSessionConfiguration.default
if let userAgent = Settings.Generic.userAgent {
sessionConfiguration.httpAdditionalHeaders = ["User-Agent": userAgent]
}
let session = URLSession(configuration: sessionConfiguration)
defer { session.finishTasksAndInvalidate() } // if you're going to create `URLSession` instances, clean up after yourself
let (data, _) = try await session.data(from: url)
guard let string = String(data: data, encoding: .utf8) else {
throw HTMLError.failedToExtractHTML
}
return string
}
This is a simple, direct translation of the question’s GCD-based code to async
-await
. Unrelated to the question at hand, if you are going to create sessions like this, make sure to finishTasksAndInvalidate
, or else each URLSession
instance is going to leak a little memory.
Frankly, you really should not even be creating a new URLSession
for each request (you would create it once and reuse it). Perhaps something like:
let session: URLSession = {
let configuration: URLSessionConfiguration = .default
if let userAgent = Settings.Generic.userAgent {
configuration.httpAdditionalHeaders = ["User-Agent": userAgent]
}
return URLSession(configuration: configuration)
}()
public func getHTML(url: String) async throws -> String {
guard let url = URL(string: url) else { throw URLError(.badURL) }
let (data, _) = try await session.data(from: url)
guard let string = String(data: data, encoding: .utf8) else {
throw HTMLError.failedToExtractHTML
}
return string
}
Frankly, I'm not sure why the user agent is even an optional, but that is beyond the scope of this question. But hopefully, this illustrates how getHTML
could be refactored with async
-await
.
I might suggest watching WWDC 2021 video Meet async/await in Swift, which is a good introduction to this topic. There are other videos linked to on that page that are also illuminating.