Reputation: 123
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. 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.
The difficulty arises due to the fact that the object might not exist.
Stripping the code bare, the code looks like this.
Please note that that the example View does not take Optional
into account
struct Foo {
var name: String
}
extension Foo {
var sampleData: [Foo] = [
Foo(name: "Bar")
]
}
again, in the perfect world without Optionals it would look like this
struct Ashwagandha: View {
@StateObject var ashwagandhaVM = AshwagandhaVM()
var body: some View {
TextField("", text: $ashwagandhaVM.currentFoo.name)
}
}
I'm purposely not unwrapping the optional, making the currentFoo: Foo?
class AshwagandhaVM: ObservableObject {
@Published var currentFoo: Foo?
init() {
self.currentFoo = Foo.sampleData.first
}
}
Below are the futile undertakings to make the TextField
and Foo.name
friends, with associated errors.
TextField("", text: $ashwagandhaVM.currentFoo?.name)
gets into the cycle of fixes on adding/removing "?"/"!"
TextField("Change chatBot's name", text: $(ashwagandhaVM.currentFoo!.name)
"'$' is not an identifier; use backticks to escape it"
TextField("", text: $ashwagandhaVM.currentFoo!.name)
"Cannot force unwrap value of non-optional type 'Binding<Foo?>'"
if let asparagus = ashwagandhaVM.currentFoo.name {
TextField("", text: $asparagus.name)
}
"Cannot find $asparagus in scope"
No luck, as the String
is nested inside an Optional
; I just don't think there should be so much hassle with editing a String
.
i.e. why this question might be irrelevant
I'm re-learning about the usage of MVVM, especially how to work with nested data types. I want to check how far I can get without writing an extra CRUD layer for every property in every ViewModel in my app. If you know any better way to achieve this, hit me up.
Upvotes: 4
Views: 3135
Reputation: 164
I've written a couple generic optional Binding
helpers to address cases like this. See this thread.
It lets you do if let unwrappedBinding = $optional.withUnwrappedValue {
or TestView(optional: $optional.defaulting(to: someNonOptional)
.
Upvotes: 0
Reputation: 2832
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:
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)
}
}
Upvotes: 0
Reputation: 30569
There is a handy Binding
constructor that converts an optional binding to non-optional, use as follows:
struct ContentView: View {
@StateObject var store = Store()
var body: some View {
if let nonOptionalStructBinding = Binding($store.optionalStruct) {
TextField("Name", text: nonOptionalStructBinding.name)
}
else {
Text("optionalStruct is nil")
}
}
}
Also, MVVM in SwiftUI is a bad idea because the View
data struct is better than a view model object.
Upvotes: 3
Reputation: 684
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.
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:
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. If the backing property changes, the view will re-render (as long as it's a @Published
property of your @StateObject
or @ObservedObject
).
Upvotes: 1