英文:
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? = "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) ()
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<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()
}
}
答案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 View
s are loaded.
let _ = Self._printChanges()
If you add this to the body
of both View
s
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
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 == ""
答案3
得分: 0
感谢 @workingdogsupportUkraine 和 @loremipsum 的帮助来调查这个问题。
目前看起来像是 SwiftUI 的一个 bug。
有一些解决方法,使用默认值,但我对此不太满意,因为在复杂数据结构的情况下,为此目的创建占位实例可能会很烦人。我更喜欢另一种方法,即将 Binding<Optional<Value>>
转换为 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)
}
...
英文:
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)
}
...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论