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. 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.
The code
Stripping the code bare, the code looks like this.
Please note that that the example View does not take Optional
into account
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
struct Ashwagandha: View {
@StateObject var ashwagandhaVM = AshwagandhaVM()
var body: some View {
TextField("", text: $ashwagandhaVM.currentFoo.name)
}
}
view model
I'm purposely not unwrapping the optional, making the 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.
Optional chaining
The 'Xcode fix' way
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?>'"
The smarter way
if let asparagus = ashwagandhaVM.currentFoo.name {
TextField("", text: $asparagus.name)
}
"Cannot find $asparagus in scope"
Workarounds
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)
}
}
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)
}
}
CodePudding user response:
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.