45

My question is probably the result of a misunderstanding but I can't figure it out, so here it is:

When using a component like a TextField or any other component requiring a binding as input

TextField(title: StringProtocol, text: Binding<String>)

And a View with a ViewModel, I naturally thought that I could simply pass my ViewModel @Published properties as binding :

class MyViewModel: ObservableObject { 
     @Published var title: String
     @Published var text: String
}

// Now in my view
var body: some View {
    TextField(title: myViewModel.title, text: myViewModel.$text)
}

But I obviously can't since the publisher cannot act as binding. From my understanding, only a @State property can act like that but shouldn't all the @State properties live only in the View and not in the view model? Or could I do something like that :

class MyViewModel: ObservableObject { 
     @Published var title: String
     @State var text: String
}

And if I can't, how can I transfer the information to my ViewModel when my text is updated?

2 Answers 2

86

You were almost there. You just have to replace myViewModel.$text with $myViewModel.text.

class MyViewModel: ObservableObject {
    
    var title: String = "SwiftUI"
    
    @Published var text: String = ""
}

struct TextFieldView: View {
    
    @StateObject var myViewModel: MyViewModel = MyViewModel()
    
    var body: some View {
        TextField(myViewModel.title, text: $myViewModel.text)
    }
}

TextField expects a Binding (for text parameter) and StateObject property wrapper takes care of creating bindings to MyViewModel's properties using dynamic member lookup.

Sign up to request clarification or add additional context in comments.

3 Comments

Can you elaborate how this works? How is $myViewModel.text able to satisfy @Binding?
@MegaManX You actually have 4 different spellings. First, you have $myViewModel which is is the whole struct as an ObservedObject. The second one is the myViewModel.text, which is simply a normal string to display on a view. For the third one, we have a binding. Since we want to bind our text to a textField we are using $myViewModel.text. The fourth one is myViewModel.$text. We are using this if we want the published values that this variable can give us. We could subscribe to this with combine.
Shouldn't it be @StateObject, not @ObservedObject since you are instantiating it inside of the view?
2

If you want to change the text by calling a method, you can create a Binding yourself and then use it in a View.

var body: some View {
    TextField(title: ..., text: text)
}

private var text: Binding<String> {
    Binding(
        get: { myViewModel.text },
        set: { myViewModel.setText($0) }
    )
}

1 Comment

This approach is unidirectional - and it is clearly superior to "two-way-bindings" as demonstrated in the question. It's more like the MVI pattern. A unidirectional approach preserves encapsulation of the mutable state in the view model. Also, due to the need of the view model to observe the Two-way-bindings itself - actually reacting on the view's changes - it can quickly lead to unnecessary complicated code in the view model.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.