I have a FormView with a StateObject
whose properties I’m binding to textfields (hopefully two-way binding)
Form View
struct FormView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("name", text: $viewModel.user.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(size: 16))
...
User
struct User: Identifiable, Codable {
var id: Int
var name: String?
init(id: Int) {
self.id = id
}
}
ViewModel
class ViewModel: ObservableObject {
// I had it as `User?` but `View` doesn't like it, so had to hardcode a default value
@Published var user: User = User(id: 0)
...
}
Error
Cannot convert value of type ‘Binding<String?>’ to expected argument type ‘Binding’
Attempt
// tried to assign default values
TextField("Name", text: $viewModel.user.name ?? "")
Question,
How to resolve the optional problem: I can’t have all the properties non-optional and even the User object can be optional.
How to bind optional properties to textfields
Attempt 2
This worked for me.
TextField("Name", text: Binding(
get: { observable.user.name ?? "" },
set: { observable.user.name = $0 }))
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(size: 16))
2
Answers
The problem is because the
name
is not a binding value, theUser
struct is a binding value so it can’t resolve that. To fix it you need to make the textBindable
.Notice that I’m still using
@StateObject
here, so it is two-way binding.A few other things to note.
Structs vs Classes
Structs are non-mutable, meaning you can’t directly change a property that exists on a struct unless it is marked
@State
or@Binding
. The wrapper@Published
cannot be added to a struct because it cannot be anObservableObject
. Classes, conversely, are"Reference Types"
, which means when you pass them around through args/params, you’re getting a reference to the real object in memory. They are mutable, and their properties can be changed. They can also contain@Published
properties, if it’s markedObservableObject
.@Published vs @State vs @Binding
@Published
– exists only onclass
objects, which areobservable
.@State
– Exists on a struct, and updates the view based on the value being updated by the view or model. This will trigger a view redraw.@Binding
– Same functionality as@State
but you would pass an@State
in args/params, so that the consuming view/object can update a parent, yet maintain it’s own state.@StateObject vs @ObservedObject
@StateObject
is intended to indicate ownership of the observable object. When you mark a property with @StateObject, SwiftUI will create and exclusively manage the lifecycle of the object, much in the way it does for @State.Remember, @StateObject should be used only for objects that are owned by this view.
@ObservedObject
, on the other hand, merely establishes a connection to an observable object without implying ownership. The object in question is expected to be created and owned elsewhere in your program, not within the view that observes it. Because if you use this, it lacks ownership, when you update from your view it will not explicitly update the same way that @State would. Only when the owning object updates it would it then update any views that use is. Ergo why it gives the appearance ofone-way
.Final Note:
Binding<Type>
such asBinding<Int>
. You can use that anywhere you’d normally use it such as an initializer. Sometimes you might run into an issue where you pass anBinding<Any>
as a parameter and you need to set it to a bound value, or initial value for State. You can useBinding(initialValue: _)
orState(initialValue: _)
orPublished(InitialValue: _)
to accommodate initialization w/ those wrapped properties.The problem you are seeing has to do with the Optional String Binding you are trying to pass into the TextField. TextField is expecting a String Binding (even if its empty), so to get that to be valid, you need the
name
of the User to be non-optional OR you need to create a Binding from an optional String.You can do this in a few ways, but one quick way would be to create an extension that does it for us:
That should be enough to let you use it in your TextField like so:
Also, as a side note I would mention that your ViewModel holds a User, so you should be injecting it. I imagine at some point, you will be able to either select different users, or maybe you are going into a specific View that adjust the User information. If you update your ViewModel, you can accept a user in like so:
And finally, your View can then be initialized with a ViewModel. This means that you would create the ViewModel before you are shown the FormView so that the data you are manipulating is coming from the Model (or the source of truth):
All of this should get you to the point where whenever you want to see the FormView, all you need to do is get the User and pass it in – or if you are creating a user, you can have it have default values:
New User:
Existing User: