Writing some SwiftUI code to handle user login with an asynchronous API, I came up with a technique to initiate the network task, and handle possible errors thrown by it.
My first attempt was to build an AsyncButton is a view built on Button that calls the supplied action inside a Task
struct AsyncButton<Label: View>: View {
@Binding public var error : Error?
public var action : () async throws -> Void
@ViewBuilder public var label : () -> Label
}
The button calls action inside a Task, and if an error is thrown, it sets error to the thrown error.
This is then used by a view like this:
struct UserLoginForm: View {
@State var login : String = ""
@State var password : String = ""
@State var error : AsyncError?
var body: some View {
NavigationStack {
VStack {
TextField("Login", text: self.$login)
SecureField("Password", text: self.$password)
if let error = self.error, case API.Errors.invalidCredentials = error {
Text("Check your credentials and try again")
}
AsyncButton(error: self.$error) {
try await self.actions.login(with: self.login, password: self.password)
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Login")
}
}
}
.onChange(of: self.error) { inError in
print("Error is \(String(describing: inError))")
}
}
@EnvironmentObject var actions : Actions
@Environment(\.presentationMode) var presentationMode
}
Unfortunately, this doesn't work, because .onChange(of: self.error) fails to compile, because onChange(of:) requires that Error conform to Equatable, and it can’t.
I worked around this issue by instead using a struct for the binding:
struct AsyncErrorWrapper {
let error: Error
}
But I couldn't make that equatable until I added an id: UUID property, and use that for the Equatable conformance.
All of that strikes me as cumbersome, at best. I don’t have a good understanding of the recent work on existentials. I'm not sure changing the declaration would automagically fix the need for Equatable conformance, but it seems to me it should be possible for SwiftUI to figure out if someone assigned something to error.
In any case, is there more elegant way to handle this?