skip to Main Content

I have a binding with optional String as a type and in the parent view I have if condition which checks whether it is has value or not. Depending on this condition I show or hide the child view. When I make name value nil the app is crashing, below you find code example.

class Model: ObservableObject {

    @Published var name: String? = "name"
    
    func makeNameNil() {
        name = nil
    }
    
}

struct ParentView: View {
    
    @StateObject var viewModel = Model()
    
    var nameBinding: Binding<String?> {
        Binding {
            viewModel.name
        } set: { value in
            viewModel.name = value
        }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Name is (viewModel.name ?? "nil")")
            Button("Make name nil") {
                viewModel.makeNameNil()
            }
            if let name = Binding(nameBinding) {  /* looks like */
                ChildView(selectedName: name) /* this causes the crash*/
            }
        }
        .padding()
    }
}

struct ChildView: View {
    
    @Binding var selectedName: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Selected name: (selectedName)")
            HStack {
                Text("Edit:")
                TextField("TF", text: $selectedName)
            }
        }
    }
}

Here is stack of the crash.

Thread 1: EXC_BREAKPOINT (code=1, subcode=0x107e1745c)

AG::Graph::UpdateStack::update() ()
AG::Graph::update_attribute(AG::data::ptr<AG::Node>, unsigned int) ()
AG::Subgraph::update(unsigned int) ()

call stack

Looks like a switfui bug for me, should I avoid using such constructions?

3

Answers


  1. Chosen as BEST ANSWER

    Thanks to @workingdogsupportUkraine and @loremipsum for help to investigate the issue.

    At the moment it looks like SwiftUI bug.

    There are some workarounds using a default value which I'm not happy with because in case of complex data structure it can be annoying to create placeholder instance for such purpose. I prefer another approach where we convert Binding<Optional<Value>> to Optional<Binding<Value>>.

    var nameBinding: Binding<String>? {
        guard let name = viewModel.name else { return nil }
        return Binding {
            name
        } set: { value in
            viewModel.name = value
        }
    }
    
    ...
       if let name = nameBinding {
           ChildView(selectedName: name)
       }
    ... 
    

  2. You could try this alternative approach to have a binding to your optional name: String?.
    It uses Binding<String>(...) as shown in the code. Works for me.

    struct ContentView: View {
        var body: some View {
            ParentView()
        }
    }
    
    class Model: ObservableObject {
        @Published var name: String? = "name"
    }
    
    struct ParentView: View {
        @StateObject var viewModel = Model()
        
        var body: some View {
            VStack(alignment: .leading, spacing: 8) {
                Text("Name is (viewModel.name ?? "nil")")
                Button("Make name nil") {
                    viewModel.name = nil // <-- here
                }
                // -- here
                if viewModel.name != nil {
                    ChildView(selectedName: Binding<String>(
                        get: { viewModel.name ?? "nil" },
                        set: { viewModel.name = $0 })
                    )
                }
            }
            .padding()
        }
    }
    
    struct ChildView: View {
        @Binding var selectedName: String
        
        var body: some View {
            VStack(alignment: .leading, spacing: 8) {
                Text("Selected name: (selectedName)")
                HStack {
                    Text("Edit:")
                    TextField("TF", text: $selectedName)
                }
            }
        }
    }
    

    EDIT-1

    You can of course use this example of code, closer to your original code. Since nameBinding is already a binding (modified now with String), having if let name = Binding(nameBinding) ... , that is, a binding of a binding optional, is not correct.

    struct ParentView: View {
        @StateObject var viewModel = Model()
        
        var nameBinding: Binding<String> {  // <-- here
            Binding {
                viewModel.name ?? "nil"  // <-- here
            } set: { value in
                viewModel.name = value
            }
        }
        
        var body: some View {
            VStack(alignment: .leading, spacing: 8) {
                Text("Name is (viewModel.name ?? "nil")")
                Button("Make name nil") {
                    viewModel.name = nil
                }
                ChildView(selectedName: nameBinding)  // <-- here
            }
            .padding()
        }
    }
    
    Login or Signup to reply.
  3. There is an undocumented method by Apple that allows you to see how, what, when SwiftUI Views are loaded.

    let _ = Self._printChanges()
    

    If you add this to the body of both Views

    struct BindingCheckView: View {
        @StateObject var viewModel = Model()
        var nameBinding: Binding<String?> {
            Binding {
                viewModel.name
            } set: { value in
                viewModel.name = value
            }
        }
        var body: some View {
            let _ = Self._printChanges()
            VStack(alignment: .leading, spacing: 8) {
                Text("Name is (viewModel.name ?? "nil")")
                Button("Make name nil") {
                    viewModel.makeNameNil()
                }
                if viewModel.name != nil{
                    ChildView(selectedName: $viewModel.name ?? "")
                }
            }
            .padding()
        }
    }
    
    struct ChildView: View {
        @Binding var selectedName: String
        var body: some View {
            let _ = Self._printChanges()
            VStack(alignment: .leading, spacing: 8) {
                Text("Selected name: (selectedName)")
                HStack {
                    Text("Edit:")
                    TextField("TF", text: $selectedName)
                }
            }
        }
    }
    

    You will see something like

    enter image description here

    You will notice that the child is being redrawn before the parent.

    So for a split second you are trying to set a non-Optional String to an Optional<String>

    I would submit this as a bug report because Apple has addressed similar issues before in order to stabilize Binding but to address your immediate issue I would use an optional binding solution from here or a combination of both.

    Or a little bit different set of solutions that combines the solutions from there

    ///This method returns nil if the `description` `isEmpty` instead of `rhs` or `default`
    func ??<T: CustomStringConvertible>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
        Binding(
            get: { lhs.wrappedValue ?? rhs },
            set: {
                lhs.wrappedValue = $0.description.isEmpty ? nil : $0
            }
        )
    }
    

    with the option above if name == "" it will change to name == nil

    ///This is for everything that doesn't conform to `CustomStringConvertible` there is no way to set `nil` from here. Same from link above.
    func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
        Binding(
            get: { lhs.wrappedValue ?? rhs },
            set: { lhs.wrappedValue = $0 }
        )
    }
    

    with the option above if name == "" it will stay name == "" and name == nil will look like name == ""

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search