Binding with optional Value causes runtime crash

huangapple go评论73阅读模式
英文:

Binding with optional Value causes runtime crash

问题

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) {
                ChildView(selectedName: name)
            }
        }
        .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) ()

It appears to be a SwiftUI bug. You may consider avoiding such constructions or looking for possible workarounds until the bug is resolved.

英文:

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? = &quot;name&quot;
    
    func makeNameNil() {
        name = nil
    }
    
}

struct ParentView: View {
    
    @StateObject var viewModel = Model()
    
    var nameBinding: Binding&lt;String?&gt; {
        Binding {
            viewModel.name
        } set: { value in
            viewModel.name = value
        }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Name is \(viewModel.name ?? &quot;nil&quot;)&quot;)
            Button(&quot;Make name nil&quot;) {
                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(&quot;Selected name: \(selectedName)&quot;)
            HStack {
                Text(&quot;Edit:&quot;)
                TextField(&quot;TF&quot;, 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&lt;AG::Node&gt;, unsigned int) ()
AG::Subgraph::update(unsigned int) ()

Binding with optional Value causes runtime crash

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

答案1

得分: 1

你可以尝试这种替代方法,将绑定到你的可选 name: String?。它使用 Binding<String>(...),如代码中所示。对我有效。

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)
            }
        }
    }
}

编辑1:

当然,你可以使用这个代码示例,更接近你的原始代码。由于 nameBinding 已经是一个绑定(现在修改为 String),使用 if let name = Binding(nameBinding) ...,也就是绑定的可选绑定,是不正确的。

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()
    }
}
英文:

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

struct ContentView: View {
    var body: some View {
        ParentView()
    }
}

class Model: ObservableObject {
    @Published var name: String? = &quot;name&quot;
}

struct ParentView: View {
    @StateObject var viewModel = Model()
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Name is \(viewModel.name ?? &quot;nil&quot;)&quot;)
            Button(&quot;Make name nil&quot;) {
                viewModel.name = nil // &lt;-- here
            }
            // -- here
            if viewModel.name != nil {
                ChildView(selectedName: Binding&lt;String&gt;(
                    get: { viewModel.name ?? &quot;nil&quot; },
                    set: { viewModel.name = $0 })
                )
            }
        }
        .padding()
    }
}

struct ChildView: View {
    @Binding var selectedName: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Selected name: \(selectedName)&quot;)
            HStack {
                Text(&quot;Edit:&quot;)
                TextField(&quot;TF&quot;, 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&lt;String&gt; {  // &lt;-- here
        Binding {
            viewModel.name ?? &quot;nil&quot;  // &lt;-- here
        } set: { value in
            viewModel.name = value
        }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Name is \(viewModel.name ?? &quot;nil&quot;)&quot;)
            Button(&quot;Make name nil&quot;) {
                viewModel.name = nil
            }
            ChildView(selectedName: nameBinding)  // &lt;-- here
        }
        .padding()
    }
}

答案2

得分: 1

以下是您要翻译的内容:

有一种由Apple提供的未记录方法,允许您查看SwiftUI“View”何时、如何加载。

let _ = Self._printChanges()

如果将此添加到两个“View”的“body”中:

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)
            }
        }
    }
}

您将会看到类似的内容:

您会注意到子视图在父视图之前被重新绘制。

因此,瞬间,您正在尝试将非“Optional”“String”设置为“Optional

我会将此作为错误报告提交,因为Apple以前已经解决了类似的问题,以稳定“Binding”,但为了解决您的即时问题,我会使用来自这里的可选绑定解决方案,或者两者的组合。

或者稍微不同的一组解决方案,结合了上面链接中的解决方案:

///如果“description”“isEmpty”,则此方法返回nil,而不是“rhs”或“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
        }
    )
}

使用上面的选项,如果name == "",它将变为name == nil

///对于不符合“CustomStringConvertible”的所有内容,无法从此处设置“nil”。与上面的链接相同。
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

使用上面的选项,如果name == "",它将保持name == "",而name == nil将看起来像name == ""

英文:

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&lt;String?&gt; {
        Binding {
            viewModel.name
        } set: { value in
            viewModel.name = value
        }
    }
    var body: some View {
        let _ = Self._printChanges()
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Name is \(viewModel.name ?? &quot;nil&quot;)&quot;)
            Button(&quot;Make name nil&quot;) {
                viewModel.makeNameNil()
            }
            if viewModel.name != nil{
                ChildView(selectedName: $viewModel.name ?? &quot;&quot;)
            }
        }
        .padding()
    }
}

struct ChildView: View {
    @Binding var selectedName: String
    var body: some View {
        let _ = Self._printChanges()
        VStack(alignment: .leading, spacing: 8) {
            Text(&quot;Selected name: \(selectedName)&quot;)
            HStack {
                Text(&quot;Edit:&quot;)
                TextField(&quot;TF&quot;, text: $selectedName)
            }
        }
    }
}

You will see something like

Binding with optional Value causes runtime crash

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&lt;String&gt;

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 ??&lt;T: CustomStringConvertible&gt;(lhs: Binding&lt;Optional&lt;T&gt;&gt;, rhs: T) -&gt; Binding&lt;T&gt; {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: {
            lhs.wrappedValue = $0.description.isEmpty ? nil : $0
        }
    )
}

with the option above if name == &quot;&quot; it will change to name == nil

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

with the option above if name == &quot;&quot; it will stay name == &quot;&quot; and name == nil will look like name == &quot;&quot;

答案3

得分: 0

感谢 @workingdogsupportUkraine 和 @loremipsum 的帮助来调查这个问题。

目前看起来像是 SwiftUI 的一个 bug。

有一些解决方法,使用默认值,但我对此不太满意,因为在复杂数据结构的情况下,为此目的创建占位实例可能会很烦人。我更喜欢另一种方法,即将 Binding&lt;Optional&lt;Value&gt;&gt; 转换为 Optional&lt;Binding&lt;Value&gt;&gt;

var nameBinding: Binding&lt;String&gt;? {
    guard let name = viewModel.name else { return nil }
    return Binding {
        name
    } set: { value in
        viewModel.name = value
    }
}

...
   if let name = nameBinding {
       ChildView(selectedName: name)
   }
... 
英文:

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&lt;Optional&lt;Value&gt;&gt; to Optional&lt;Binding&lt;Value&gt;&gt;.

var nameBinding: Binding&lt;String&gt;? {
    guard let name = viewModel.name else { return nil }
    return Binding {
        name
    } set: { value in
        viewModel.name = value
    }
}

...
   if let name = nameBinding {
       ChildView(selectedName: name)
   }
... 

huangapple
  • 本文由 发表于 2023年5月13日 13:32:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76241232.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定