简体   繁体   English

当 SwiftUI 拒绝时使用可选绑定

[英]Using optional binding when SwiftUI says no

The problem问题

TL;DR: A String I'm trying to bind to inside TextField is nested in an Optional type, therefore I cannot do that in a straightforward manner. TL;DR:我试图绑定到TextField内部的String嵌套在Optional类型中,因此我无法直接执行此操作。 I've tried various fixes listed below.我已经尝试了下面列出的各种修复。

I'm a simple man and my use case is rather simple - I want to be able to use TextField to edit my object's name .我是一个简单的人,我的用例也很简单——我希望能够使用 TextField 来编辑我的对象的名称
The difficulty arises due to the fact that the object might not exist .困难的产生是因为object 可能不存在

The code代码

Stripping the code bare, the code looks like this.剥离代码,代码看起来像这样。
Please note that that the example View does not take Optional into account请注意,示例 View没有考虑Optional

model model

struct Foo {
  var name: String
}

extension Foo {
  var sampleData: [Foo] = [
    Foo(name: "Bar")
  ]
}

view看法

again, in the perfect world without Optionals it would look like this再次,在没有Optionals 的完美世界中,它看起来像这样

struct Ashwagandha: View {
  @StateObject var ashwagandhaVM = AshwagandhaVM()
  var body: some View {
    TextField("", text: $ashwagandhaVM.currentFoo.name)
  }
}

view model查看 model

I'm purposely not unwrapping the optional, making the currentFoo: Foo?我故意打开可选的包装,使currentFoo: Foo?

class AshwagandhaVM: ObservableObject {
  @Published var currentFoo: Foo?

  init() {
    self.currentFoo = Foo.sampleData.first
  }
}

The trial and error反复试验

Below are the futile undertakings to make the TextField and Foo.name friends, with associated errors.以下是使TextFieldFoo.name朋友的徒劳承诺,并带有相关错误。

Optional chaining可选链接

The 'Xcode fix' way “Xcode 修复”方式

TextField("", text: $ashwagandhaVM.currentFoo?.name)
gets into the cycle of fixes on adding/removing "?"/"!"进入添加/删除“?”/“!”的修复周期

The desperate way绝望的方式

TextField("Change chatBot's name", text: $(ashwagandhaVM.currentFoo..name)
"'$' is not an identifier; use backticks to escape it" “'$' 不是标识符;使用反引号将其转义”

Forced unwrapping强制展开

The dumb way愚蠢的方式

TextField("", text: $ashwagandhaVM.currentFoo..name)
"Cannot force unwrap value of non-optional type 'Binding<Foo?>'" “无法强制解包非可选类型‘Binding<Foo?>’的值”

The smarter way更聪明的方法

if let asparagus = ashwagandhaVM.currentFoo.name {
  TextField("", text: $asparagus.name)
}

"Cannot find $asparagus in scope" “在范围内找不到 $asparagus”

Workarounds解决方法

My new favorite quote's way我最喜欢的新报价方式

No luck, as the String is nested inside an Optional ;运气不好,因为String嵌套在Optional中; I just don't think there should be so much hassle with editing a String .我只是认为编辑String不应该有那么多麻烦。

The rationale behind it all这一切背后的理由

ie why this question might be irrelevant即为什么这个问题可能无关紧要

I'm re-learning about the usage of MVVM, especially how to work with nested data types.我正在重新学习 MVVM 的用法,尤其是如何使用嵌套数据类型。 I want to check how far I can get without writing an extra CRUD layer for every property in every ViewModel in my app.我想检查在不为应用程序中每个 ViewModel 中的每个属性编写额外的 CRUD 层的情况下我能走多远。 If you know any better way to achieve this, hit me up.如果您知道实现此目标的更好方法,请联系我。

Folks in the question comments are giving good advice.问题评论中的人们给出了很好的建议。 Don't do this: change your view model to provide a non-optional property to bind instead.不要这样做:更改您的视图 model 以提供一个非可选属性来代替。

But... maybe you're stuck with an optional property, and for some reason you just need to bind to it.但是......也许你被一个可选属性卡住了,出于某种原因你只需要绑定它。 In that case, you can create a Binding and unwrap by hand:在这种情况下,您可以创建一个Binding并手动解包:

class MyModel: ObservableObject {
    @Published var name: String? = nil
    
    var nameBinding: Binding<String> {
        Binding {
            self.name ?? "some default value"
        } set: {
            self.name = $0
        }
    }
}

struct AnOptionalBindingView: View {
    @StateObject var model = MyModel()
    
    var body: some View {
        TextField("Name", text: model.nameBinding)
    }
}

That will let you bind to the text field.这将使您绑定到文本字段。 If the backing property is nil it will supply a default value.如果支持属性为 nil,它将提供默认值。 If the backing property changes, the view will re-render (as long as it's a @Published property of your @StateObject or @ObservedObject ).如果支持属性更改,视图将重新呈现(只要它是您的@StateObject@ObservedObject@Published属性)。

I think you should change approach, the control of saving should remain inside the model, in the view you should catch just the new name and intercept the save button coming from the user:我认为你应该改变方法,保存的控制应该保留在 model 中,在视图中你应该只捕获新名称并拦截来自用户的保存按钮:

在此处输入图像描述

class AshwagandhaVM: ObservableObject {
    @Published var currentFoo: Foo?

    init() {
        self.currentFoo = Foo.sampleData.first
    }
    func saveCurrentName(_ name: String) {
        if currentFoo == nil {
            Foo.sampleData.append(Foo(name: name))
            self.currentFoo = Foo.sampleData.first(where: {$0.name == name})
        }
        else {
            self.currentFoo?.name = name
        }
    }
}

struct ContentView: View {
    @StateObject var ashwagandhaVM = AshwagandhaVM()
    @State private var textInput = ""
    @State private var showingConfirmation = false
    
    var body: some View {
        VStack {
            TextField("", text: $textInput)
                .padding()
                .textFieldStyle(.roundedBorder)
            Button("save") {
                showingConfirmation = true
            }
            .padding()
            .buttonStyle(.bordered)
            .controlSize(.large)
            .tint(.green)
            .confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
                Button("Yes") {
                    confirmAndSave()
                }
                Button("No", role: .cancel) { }
            }
            //just to check
            if let name = ashwagandhaVM.currentFoo?.name {
                Text("in model: \(name)")
                    .font(.largeTitle)
            }
        }
        .onAppear() {
            textInput = ashwagandhaVM.currentFoo?.name ?? "default"
        }
    }
    
    func confirmAndSave() {
        ashwagandhaVM.saveCurrentName(textInput)
    }
}

UPDATE更新

do it with whole struct用整个结构做

struct ContentView: View {
    @StateObject var ashwagandhaVM = AshwagandhaVM()
    @State private var modelInput = Foo(name: "input")
    @State private var showingConfirmation = false
    
    var body: some View {
        VStack {
            TextField("", text: $modelInput.name)
                .padding()
                .textFieldStyle(.roundedBorder)
            Button("save") {
                showingConfirmation = true
            }
            .padding()
            .buttonStyle(.bordered)
            .controlSize(.large)
            .tint(.green)
            .confirmationDialog("are you sure?", isPresented: $showingConfirmation, titleVisibility: .visible) {
                Button("Yes") {
                    confirmAndSave()
                }
                Button("No", role: .cancel) { }
            }
            //just to check
            if let name = ashwagandhaVM.currentFoo?.name {
                Text("in model: \(name)")
                    .font(.largeTitle)
            }
        }
        .onAppear() {
            modelInput = ashwagandhaVM.currentFoo ?? Foo(name: "input")
        }
    }
    
    func confirmAndSave() {
        ashwagandhaVM.saveCurrentName(modelInput.name)
    }
}
struct ContentView: View {
   @StateObject var store = Store()

   var body: some View {
    if let nonOptionalStructBinding = Binding($store.optionalStruct)
        TextField("", text: nonOptionalStructBinding.name)
    }
    else {
        Text("optionalStruct does not exist")
    }
  }
}

Also, MVVM in SwiftUI is a bad idea because the View data struct is better than a view model object.此外,SwiftUI 中的 MVVM 是一个坏主意,因为View数据结构比视图 model object 更好。

I've written a couple nice generic optional Binding helpers to address cases like this.我已经编写了几个不错的通用可选Binding助手来解决这种情况。 See this thread .请参阅此线程

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM