如何绑定 SwiftUI 中作为可选项的一部分的数据

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

How to bind to data that's part of an optional in SwiftUI

问题

我好奇,我们如何指定绑定到可选项的一部分状态数据?例如:

struct NameRecord {
    var name = ""
    var isFunny = false
}

class AppData: ObservableObject {
    @Published var nameRecord: NameRecord?
}

struct NameView: View {
    @StateObject var appData = AppData()
    
    var body: some View {
        Form {
            if appData.nameRecord != nil {
                // 在这一点上,我知道nameRecord不是nil,所以
                //

<details>
<summary>英文:</summary>

I&#39;m curious, how do we specify a binding to State data that is part of an optional? For instance:

struct NameRecord {
var name = ""
var isFunny = false
}

class AppData: ObservableObject {
@Published var nameRecord: NameRecord?
}

struct NameView: View {
@StateObject var appData = AppData()

var body: some View {
    Form {
        if appData.nameRecord != nil {
            // At this point, I *know* that nameRecord is not nil, so
            // I should be able to bind to it.
            TextField(&quot;Name&quot;, text: $appData.nameRecord.name)
            Toggle(&quot;Is Funny&quot;, isOn: $appData.nameRecord.isFunny)
        } else {
            // So far as I can tell, this should never happen, but
            // if it does, I will catch it in development, when
            // I see the error message in the constant binding.
            TextField(&quot;Name&quot;, text: .constant(&quot;ERROR: Data is incomplete!&quot;))
            Toggle(&quot;Is Funny&quot;, isOn: .constant(false))
        }
    }
    .onAppear {
        appData.nameRecord = NameRecord(name: &quot;John&quot;)
    }
}

}


I can certainly see that I&#39;m missing something. Xcode gives errors like `Value of optional type &#39;NameRecord?&#39; must be unwrapped to refer to member &#39;name&#39; of wrapped base type &#39;NameRecord&#39;`) and offers some FixIts that don&#39;t help.

Based on the answer from the user &quot;workingdog support Ukraine&quot; I now know how to make a binding to the part I need, but the solution doesn&#39;t scale well for a record that has many fields of different type.

Given that the optional part is in the middle of `appData.nameRecord.name`, it seems that there might be a solution that does something like what the following function in the SwiftUI header might be doing:

public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }


My SwiftFu is insufficient, so I don&#39;t know how this works, but I suspect it&#39;s what is doing the work for something like `$appData.nameRecord.name` when nameRecord is not an optional. I would like to have something where this function would result in a binding to `.constant` when anything in the keyPath is nil (or even if it did a fatalError that I would avoid with conditionals as above). It would be great if there was a way to get a solution that was as elegant as [Jonathan&#39;s answer][1] that was also suggested by  workingdog for a similar situation. Any pointers in that area would be much appreciated!


  [1]: https://stackoverflow.com/a/61002589/14840926

</details>


# 答案1
**得分**: 3

[`Binding`有一个可失败的初始化器,用于转换`Binding&lt;Value?&gt;`](https://developer.apple.com/documentation/swiftui/binding/init(_:)-5z9t9)

```swift
if let nameRecord = Binding($appData.nameRecord) {
  TextField("Name", text: nameRecord.name)
  Toggle("Is Funny", isOn: nameRecord.isFunny)
} else {
  Text("Data is incomplete")
  TextField("Name", text: .constant(""))
  Toggle("Is Funny", isOn: .constant(false))
}

或者,减少重复的代码:

if appData.nameRecord == nil {
  Text("Data is incomplete")
}

let bindings = Binding($appData.nameRecord).map { nameRecord in
  ( name: nameRecord.name,
    isFunny: nameRecord.isFunny
  )
} ?? (
  name: .constant(""),
  isFunny: .constant(false)
)

TextField("Name", text: bindings.name)
Toggle("Is Funny", isOn: bindings.isFunny)
英文:

Binding has a failable initializer that transforms a Binding&lt;Value?&gt;.

if let nameRecord = Binding($appData.nameRecord) {
  TextField(&quot;Name&quot;, text: nameRecord.name)
  Toggle(&quot;Is Funny&quot;, isOn: nameRecord.isFunny)
} else {
  Text(&quot;Data is incomplete&quot;)
  TextField(&quot;Name&quot;, text: .constant(&quot;&quot;))
  Toggle(&quot;Is Funny&quot;, isOn: .constant(false))
}

Or, with less repetition:

if appData.nameRecord == nil {
  Text(&quot;Data is incomplete&quot;)
}

let bindings = Binding($appData.nameRecord).map { nameRecord in
  ( name: nameRecord.name,
    isFunny: nameRecord.isFunny
  )
} ?? (
  name: .constant(&quot;&quot;),
  isFunny: .constant(false)
)

TextField(&quot;Name&quot;, text: bindings.name)
Toggle(&quot;Is Funny&quot;, isOn: bindings.isFunny)

答案2

得分: 1

First of all, full props to @Jessy for the essence of the solution in his responses. I'm posting this answer as an alternative that gives a concise answer to the question, but also includes other information for readers.

Essence of Solution

Binding has a failing initializer for an optional value, that fails (ie returns a nil) if the optional value is nil, but otherwise returns a normal binding to the wrapped value.

For example:

let nameBinding = Binding($appData.nameRecord)
if let nameBinding {
    TextField("Name", text: nameBinding.name)    // note absence of $ here
} else {
    Text("SwiftUI cannot bind to appData.nameRecord, because it is nil!")
}

All that remains is to decide on a good way to use this solution in our code.

Concise Implementation

struct NameView: View {
    @StateObject var appData = AppData()

    var body: some View {
        let nameBinding = Binding($appData.nameRecord) ?? .constant(.init(name: "DATA ERROR!"))
        Form {
            TextField("Name", text: nameBinding.name)
            Toggle("Is Funny", isOn: nameBinding.isFunny)
        }
        .onAppear {
            appData.nameRecord = NameRecord(name: "John")
        }
    }
}

To explain, in this particular situation, it is pretty much guaranteed that the optional is never nil (given the code in .onAppear), so we don't have to spend much code dealing with the edge-case of the failed binding. So we just structure the code so that the nil-case is dealt with immediately by using a binding to a constant NameRecord with an error message inserted into the data whenever data-that-should-never-be-nil is (somehow) nil. You can view what this would look like to the user if you comment out the .onAppear code.

This method makes the code concise and lets you clearly see the SwiftUI structures that are most relevant to you.

Alternate Implementation

The above method is pretty safe, assuming that onAppear works as we expect. But perhaps you want to account for the possibility that it doesn't (eg due to an unexpected change in how onAppear works in the future) or simply because your data structures don't lend themselves to the above solution. In those cases, you could consider something like this:

struct NameView: View {
    @StateObject var appData = AppData()

    var body: some View {
        let nameBinding = Binding($appData.nameRecord)
        Form {
            if let nameBinding {
                TextField("Name", text: nameBinding.name)
                Toggle("Is Funny", isOn: nameBinding.isFunny)
            } else {
                Text("DATA ERROR!")
            }
        }
        .onAppear {
            appData.nameRecord = NameRecord(name: "John")
        }
    }
}

This alternate implementation gives us an elegant way of replacing our entire UI with whatever message we want to give the user in the failing situation. The only price we have to pay is the additional noise from an extra conditional in our code.

Further Reading

If this is the first time you are seeing a constant binding, please watch the 2020 WWDC video Structure your app for SwiftUI previews.

Make sure you are comfortable with adding custom bindings, as they will be the solution to a whole class of tricky SwiftUI binding situations, including the above situation (though less favored as too complicated, here).

For situations where you've got @State data that is optional, there's an even simpler solution you can use.

英文:

First of all, full props to @Jessy for the essence of the solution in his responses. I'm posting this answer as an alternative that gives a concise answer to the question, but also includes other information for readers.

Essence of Solution

Binding has a failing initializer for an optional value, that fails (ie returns a nil) if the optional value is nil, but otherwise returns a normal binding to the wrapped value.

For example:

let nameBinding = Binding($appData.nameRecord)
if let nameBinding {
    TextField(&quot;Name&quot;, text: nameBinding.name)    // note absence of $ here
} else {
    Text(&quot;SwiftUI cannot bind to appData.nameRecord, because it is nil!&quot;)
}

All that remains is to decide on a good way to use this solution in our code.

Concise Implementation

struct NameView: View {
    @StateObject var appData = AppData()
    
    var body: some View {
        let nameBinding = Binding($appData.nameRecord) ?? .constant(.init(name: &quot;DATA ERROR!&quot;))
        Form {
            TextField(&quot;Name&quot;, text: nameBinding.name)
            Toggle(&quot;Is Funny&quot;, isOn: nameBinding.isFunny)
        }
        .onAppear {
            appData.nameRecord = NameRecord(name: &quot;John&quot;)
        }
    }
}

To explain, in this particular situation, it is pretty much guaranteed that the optional is never nil (given the code in .onAppear), so we don't have to spend much code dealing with the edge-case of the failed binding. So we just structure the code so that the nil-case is dealt with immediately by using a binding to a constant NameRecord with an error message inserted into the data whenever data-that-should-never-be-nil is (somehow) nil. You can view what this would look like to the user if you comment out the .onAppear code.

This method makes the code concise and lets you clearly see the SwiftUI structures that are most relevant to you.

Alternate Implementation

The above method is pretty safe, assuming that onAppear works as we expect. But perhaps you want to account for the possibility that it doesn't (eg due to an unexpected change in how onAppear works in the future) or simply because your data structures don't lend themselves to
the above solution. In those cases, you could consider something like this:

struct NameView: View {
    @StateObject var appData = AppData()
    
    var body: some View {
        let nameBinding = Binding($appData.nameRecord)
        Form {
            if let nameBinding {
                TextField(&quot;Name&quot;, text: nameBinding.name)
                Toggle(&quot;Is Funny&quot;, isOn: nameBinding.isFunny)
            } else {
                Text(&quot;DATA ERROR!&quot;)
            }
        }
        .onAppear {
            appData.nameRecord = NameRecord(name: &quot;John&quot;)
        }
    }
}

This alternate implementation gives us an elegant way of replacing our entire UI with whatever message we want to give the user in the failing situation. The only price we have to pay is the additional noise from an extra conditional in our code.

Further Reading

If this is the first time you are seeing a constant binding, please watch the 2020 WWDC video Structure your app for SwiftUI previews

Make sure you are comfortable with adding custom bindings, as they will be the solution to a whole class of tricky swiftUI binding situations, including the above situation (though less favoured as too complicated, here).

For situations where you've got @State data that is optional, there's an even simpler solution you can use.

huangapple
  • 本文由 发表于 2023年2月6日 09:54:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/75356738.html
匿名

发表评论

匿名网友

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

确定