When a SwiftUI view has a complex logic, i like to employ an enum to represent it's state (often in a viewmodel/service).
struct FooView {
enum ProgressState {
case idle
case loading
case completed(Model)
case error
var stateName: String { ... }
}
@State var state: ProgressState = .idle
...
PROBLEM:
What i'd like tho, is a way or methodology to connect bindables to a specific enum case. In this example, to show an alert (or a sheet), since something like <<$state.error>>
is obviously not possible.
...
var body: some View {
Text(state.stateName)
// show alert on error
.alert("oops there was an error",
isPresented: <<$state.error>>)
{ //1.
...
}
}
}
CURRENT ALTERNATIVE
The easiest solution is to use additional variables, but i really do not like to keep them synchronised manually.
BETTER SOLUTIONS?
I was exploring using combine to update the additional variable upon state
var changes, the problem is always that the bindable needs to change it's state as well, but in this case the alert, does not know to which state to bring it back to.
So maybe a protocol for the enum and an extension to alert and sheet to handle these instead of Bindable ? Has anybody tried a similar approach or know a better one?
Thank you
SwiftUI provides a solution to present LocalizedError
func alert<E, A>(
isPresented: Binding<Bool>,
error: E?,
@ViewBuilder actions: () -> A
) -> some View where E : LocalizedError, A : View
You can create a simple enum
that can present any standard Error
or custom errors.
enum LocalError: LocalizedError {
//Use for any built in error
case error(Error)
//Use for something custom
case invalidId
var errorDescription: String? {
switch self {
case .error(let error):
return error.localizedDescription
case .invalidId:
return "\(self)"
}
}
var recoverySuggestion: String? {
switch self {
case .error(let error):
let nsError = error as NSError
return nsError.localizedRecoverySuggestion
default:
return nil
}
}
}
Then you can modify your progress enum.
enum ProgressState {
case idle
case loading
case completed(Model)
case error(Error)
You can then trigger an alert
(or sheet) when that state is triggered
switch state {
case .error(let error):
Text(state.stateName)
.task {
alert = (true, .error(error))
}
default :
Text(state.stateName)
}
Here is the full code.
import SwiftUI
struct FooView: View {
enum ProgressState {
case idle
case loading
case completed(Model)
case error(LocalError)
var stateName: String {
switch self {
case .completed(_):
return "Complete"
case .error(_):
return "Something went wrong"
default:
return "\(self)"
}
}
}
@State private var state: ProgressState = .idle
@State private var alert: (isPresented: Bool, error: LocalError?) = (false, nil)
var body: some View {
Group {
switch state {
case .error(let error):
Text(state.stateName)
.task {
alert = (true, error)
}
default :
Text(state.stateName)
}
}.alert(isPresented: $alert.isPresented, error: alert.error) {
Button("Ok") {
alert = (false, nil)
}
}
.task {
try? await Task.sleep(for: .seconds(1))
state = .error(.invalidId)
}
}
struct Model {
}
enum LocalError: LocalizedError {
//Use for any built in error
case error(Error)
//Use for something custom
case invalidId
var errorDescription: String? {
switch self {
case .error(let error):
return error.localizedDescription
case .invalidId:
return "\(self)"
}
}
var recoverySuggestion: String? {
switch self {
case .error(let error):
let nsError = error as NSError
return nsError.localizedRecoverySuggestion
default:
return nil
}
}
}
}
#Preview {
FooView()
}
This provides an independent alert
variable so the presentation of the .alert
does not conflict with the View
behind it or make any unsafe assumptions.