1

I have a very simple SwiftUI view that only shows a TextField. The text field's text is bound to the string property of my viewModel that I instantiate as a @StateObject:

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        TextField("Placeholder", text: $viewModel.string)
            .padding()
    }
}

The one thing that's "out of the ordinary" is that my string property is not a @Published property, but a simple computed property. In its setter, it sets the displayedString property to a fixed string (for testing purposes) which is a @Published property:

class ViewModel: ObservableObject {
    @Published var displayedString: String = ""

    var string: String {
        get { displayedString }
        set { displayedString = "OVERRIDE" }
    }
}

So when I type a string into the text field, I would expect the setter to trigger a view update (as it updates a @Published property) and then in the view, the text field should be updated with the displayedString. So in other words: No matter what I type, the text field should always show the string OVERRIDE.

But that's not the case!

It works for the very first letter I type, but then I can freely type anything into the text field.

For example, if I launch the app with only this view and type the string 123456789 this is what is displayed on the text field: OVERRIDE23456789

Why is that?

And how can I get the text field to always be up-to-date to what's set on the viewModel? (I know, I can just hook the text field to a @Published property directly, but I'm doing this computed property stuff for a reason and want to understand why it doesn't work as expected.)


Additional Observation:

When I add a Text label above the TextField and just make it show the viewModel.string, it shows the correct (expected) string:

var body: some View {
    Text(viewModel.string)
    TextField("Placeholder", text: $viewModel.string)
        .padding()
}

Screenshot

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • Because `displayedString` is not changed after first time, ie `OVERRIDE == OVERRIDE` - no update event. – Asperi Jan 19 '22 at 14:40
  • If you put `Text(viewModel.displayedString)` after it, you can see that `displayedString` is always "OVERRIDE" after the first letter. It looks like `TextField` is assuming that it is updating the computed variable, even though it can't. I would recommend making your own specialized `TextField` that handles whatever case you want. – Yrb Jan 19 '22 at 14:42
  • @Asperi But the view itself *is* updated (as you can see in the "Additional Observation". The `body` closure is called again each time I type a character. So `TextField` must have some sort of an internal state I guess? – Mischa Jan 19 '22 at 14:48

1 Answers1

1

I’m not sure but I think a custom binding might work for you

In the ViewModel use

var string: Binding<String> { Binding(
    get: { displayedString },
    set: { displayedString = “OVERRIDE” } )}

Then in body use: viewModel.string instead of $viewModel.string in the TextField and use: viewModel.string.wrappedValue in the text view

Stuf

Joseph Levy
  • 157
  • 8
  • Oops. I forgot the _ in at the beginning of the set: in the binding – Joseph Levy Feb 02 '22 at 21:00
  • I tried that approach as well → same result. That made me assume that the `TextField` has an internal state that is sometimes not updates corrects – with or without bindings. – Mischa Feb 03 '22 at 12:48