What is the correct design pattern for initializing SwiftUI view state from a function that can throw an exception?

I am trying to create a SwiftUI view that gets its data from the file system which involves invoking functions that can throw exceptions. Where is the correct place to execute the throwing function before setting on a @State property?

Everything I have tried so far has not worked and has felt like I'm fighting the system, which makes me suspect I'm missing a common design pattern (probably since I'm new to SwiftUI and Swift, and seems like this should be a common use case).

A throwing initializer.

init(getState: () throws -> State) throws {
  _state = .init(wrappedValue: try getState())
}

You did not tell how you intend to handle errors. Should the app crash? Maybe just use try!. Should the app display an error somewhere, in an alert, or somewhere else? Then use do { try ... } catch { ... }, or Result(catching: { try ... }) in order to make your views able to handle the extra states needed by error handling (letting the user know, letting the user click or tap acknowledgment, or repair, or retry buttons, automatic dismiss of a temporary error message, etc.) Note that I used the plural "views", instead of "view", because presenting errors to users usually involves several views: some are dedicated to the success scenario, some others are dedicated to the error scenario. In all cases, your first step is to decide how you want your app to behave. It's easier to implement something when you know what you want.

3 Likes

Expanding on @anon9791410's answer above, with this setup:

func getState() throws -> Int {
    throw NSError() // or don't throw
}

struct SomeView: View {
    @State var state: Int
    
    init() throws {
        _state = .init(wrappedValue: try getState())
    }

    var body: some View {
        Text("Hello")
    }
}

if you then try to use the standard do {} catch {} block:

struct ContentView: View {
    var body: some View {
        do { //🛑 Closure containing control flow statement cannot be used with result builder 'ViewBuilder'
            try SomeView()
        } catch {
            Color.red
        }
    }
}

that won't compile because "Closure containing control flow statement cannot be used with result builder". As a workaround you can use this:

struct ContentView: View {
    var body: some View {
        if let v = try? SomeView() {
            v
        } else {
            Color.red
        }
    }
}

Which does the trick.

1 Like

Also an option until someone fixes that:

`do`(try: SomeView.init) { _ in
   Color.red
 }

AKA

`do` {
  try SomeView()
} catch: { _ in
  Color.red
}

I really hate that you can't label the first closure there with catch.


/// A workaround for `do/catch` statements not working with result builders.
@ViewBuilder public func `do`(
  @ViewBuilder try success: () throws -> some View,
  @ViewBuilder catch failure: (any Error) -> some View
) -> some View {
  switch Result(catching: success) {
  case .success(let success): success
  case .failure(let error): failure(error)
  }
}
1 Like

Thanks @anon9791410 and @tera for the suggestion and examples.

I had started down that path before but based on this post thought it isn't an intended way to use a @State variable. Do you have any idea how stable this pattern is or if it is an intended usage of a @State property?

Thanks for the thoughts @gwendal.roue. I am still thinking through the correct design for the error handling, though am mostly convinced that however I handle it, the best user experience will involve setting some state to indicate the error so the user knows what is going on. Right now I'm conditionally showing an error Text view during prototyping.

What was confusing me about that approach was believing that @State properties were not supposed to be set in an initializer (which would apply to both my original state property and any error state properties) (see post). Based on Jessy's post, I'm going to try setting both properties in the initializer and see how it goes.

1 Like