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?