Home > database >  State within @ViewBuilder functions
State within @ViewBuilder functions

Time:08-10

I have a protocol in which implementors must have a static function which creates a view. The view then creates an object and places in into the binding location which was supplied.

protocol Creatable {
    associatedtype CreationView: View
    static func buildCreationView(toReplace: Binding<AnyMyObject>) -> CreationView
    // ...
}

In practise there is a screen in which the user selects from a list of implementors and when that selection changes, buildCreationView creates a form for all the required properties of said object and a create button which write the new object into toReplace.

When I implement buildCreationView however I need to be able to create state for this form. My naive implementation was just to use an @State property like so:

    @ViewBuilder static func buildCreationView(toReplace: Binding<AnyMyObject>) -> some View {
        @State var fieldOne: String = ""
        
        Form {
            TextField("Field One", text: $fieldOne) // Error here
            Button {
                toReplace.wrappedValue = ObjectOne(fieldOne: fieldOne).erase
            } label: {
                Text("Create Object One")
            }
        }
    }

This create a swiftUI runtime error saying: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.

Not knowing how to fix this I tried a hack of:

   var propertyOne: String = ""
   let propertyOneBinding: Binding<String> = .init {
      propertyOne
   } set: {
      propertyOne = $0
   }

And using the binding with the TextField. Which does work but is very ugly.

What is the correct way to house @State within a view building function?

CodePudding user response:

Create a View-conforming type as your CreationView, and move your @State property into it.

struct MyCreatable: Creatable {
    static func buildCreationView(toReplace: Binding<AnyMyObject>) -> CreationView {
        return CreationView(toReplace: toReplace)
    }
    
    struct CreationView: View {
        @Binding var toReplace: AnyMyObject 
        @State var fieldOne: String = ""
        
        var body: some View {
            Form {
                TextField("Field One", text: $fieldOne)
                Button {
                    toReplace = ObjectOne(fieldOne: fieldOne).erase
                } label: {
                    Text("Create Object One")
                }
            }
        }
    }
}
  • Related