I am having a hard time learning the new Swift async functions and getting them to perform in the correct order.
I want to make my loadData() function "await" or WAIT when calling it but when printing the order that should be 1,2,3 it prints out as 1,3,2 which means after calling the function it goes to the next line of code instead of waiting for the results of vm.loadData()
I am getting the warning "No 'async' operations occur within 'await' expression" where I try to make the call but I thought by putting the call in a task would make it work?
Below is my code, what am I doing wrong?
VIEW
struct Button: View {
let vm = ViewModel()
var body: some View {
Circle().onTapGesture { doSomething }
}
func doSomething() {
print("1")
Task {
// WARNING: No 'async' operations occur within 'await' expression
await vm.loadData()
}
print("3")
}
}
VIEW MODEL
class ViewModel: ObservableObject {
@Published var data: [String] = []
func loadData() {
Task {
do {
let dataArray = try await DataApi.loadData()
if !dataArray.isEmpty {
self.data = dataArraay
}
print("2")
} catch {
<HANDLE ERRORS>
}
}
}
}
API CALL
class DataApi {
static func loadData() async throws -> [String] {
// MAKE API CALL
do {
// decode data
return data
} catch {
throw ResponseError.decodingError("\(error)")
}
return []
}
}
CodePudding user response:
vm.loadData()
is not an async method, so the compiler is complaining that you call it with await
in your SwiftUI view.
At its most basic, Swift's async rule is: any time you use the await
keyword, you must be calling a method that's declared with async
, and if you declare a method with async
, you must use await
when you call it.
In ViewModel
, you're using Task
correctly as a "bridge" between synchronous mode and asynchronous mode. You call vm.loadData()
synchronously, and an async task calls the async API; when the data comes back, it stores the returned value in a property.
Creating that Task returns immediately, so vm.loadData()
finishes executing while the task is still running.
That means that your call to vm.loadData()
doesn't need to be wrapped in another task. You can call it directly, and it'll create the background async work for you. That simplifies your calling code:
Circle().onTapGesture { vm.loadData() }
CodePudding user response:
await
means only that you can proceed with the result of the line within the task. It does not mean that the enclosing task is synchronous.
The warning is displayed because loadData
in ViewModel
is not async
.
This is a complete asynchronous version
func doSomething() {
print("1")
Task {
await vm.loadData()
print("3")
}
}
ViewModel
@MainActor
class ViewModel: ObservableObject {
@Published var data: [String] = []
func loadData() async {
do {
self.data = try await DataApi.loadData()
} catch {
self.data = []
print(error)
}
print("2")
}
}
API call
class DataApi {
static func loadData() async throws -> [String] {
// try await MAKE API CALL
// try decode data
return data
}
}
It's recommended to hand over errors to the caller when marked with throws
. The API call is supposed to throw the ResponseError