Here is a code snippet from my SwiftUI ViewModel:
@Published var data: Data
@Published var showingAlert = false
func fetchData() {
Task {
let result = await NetworkManager.fetchData(),
await updateUI(with: result)
}
}
@MainActor
private func updateUI(with result: Result<Data, Error>) {
do {
data = try result.get()
} catch {
showingAlert = true
}
}
I'm not sure if this is the idiomatic approach (I'm relatively new to SwiftUI) but it works great.
My question is:
Can we get rid of one "await" by merging the lines into a single one?
await updateUI(with: NetworkManager.fetchData())
Is this code equivalent to the previous one?
Most importantly, I need the network call to run on a background thread while the UI updates are performed on the main thread. Is this still the case here?
How can it be proven? (you know, in GPT era everything should be fact checked 😂)
Yes, these are equivalent. From the Swift Evolution proposal:
Consider the following example:
// func redirectURL(for url: URL) async -> URL { ... } // func dataTask(with: URL) async throws -> (Data, URLResponse) { ... } let newURL = await server.redirectURL(for: url) let (data, response) = try await session.dataTask(with: newURL)
In this code example, a task suspension may happen during the calls to
redirectURL(for:)
anddataTask(with:)
because they are async functions. Thus, both call expressions must be contained within someawait
expression, because each call contains a potential suspension point. Anawait
operand may contain more than one potential suspension point. For example, we can use oneawait
to cover both potential suspension points from our example by rewriting it as:let (data, response) = try await session.dataTask(with: server.redirectURL(for: url))
The
await
has no additional semantics; liketry
, it merely marks that an asynchronous call is being made.
As long as NetworkManager.fetchData()
is not isolated to the main actor, it will run on a non-main thread, by the cooperative thread pool. Even if it is isolated to the main actor, you are probably using URLSession
APIs in its implementation, which are not isolated.
You can call MainActor.shared.assertIsolated()
in NetworkManager.fetchData()
to "prove" this. If this method crashes, then you are not on the main actor.
It seems like you are using an ObservableObject
for this. There is no need for an ObservableObject
just for this async operation. You can move everything into your View
.
@State var data: Data?
@State var showingAlert = false
// @MainActor // in iOS 18, the 'View' protocol is entirely isolated to the MainActor, so this will be redundant
private func updateUI(with result: Result<Data, Error>) {
do {
data = try result.get()
} catch {
showingAlert = true
}
}
func fetchData() async {
let result = await NetworkManager.fetchData(),
// similarly for the 'await' here. You don't need it in iOS 18
/*await*/ updateUI(with: result)
}
You can then call this in a .task { ... }
or task(id: something) { ... }
modifier, which cancels the task if the view disappears. Right now your ObservableObject
does not handle cancellation of the top level Task
you created.