Why is didSet called twice on the TextField binding in SwiftUI?
Asked Answered
B

3

28

I have a very basic view that only shows a TextField:

View

struct ContentView: View {

    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        TextField("Enter a string...", text: $viewModel.string)
    }
    
}

The TextField's text is bound to a string property on the view model:

ViewModel

class ViewModel: ObservableObject {
    
    @Published var string: String = "" {
        didSet {
            print("didSet string:", string)
        }
    }
    
}

I added a didSet property observer to perform a custom action whenever the string changes. For this simple example, I only print a string on the console.

Observation

When I run this code and enter the string "123" into the text field, this is the output I get:

didSet string: 1
didSet string: 1
didSet string: 12
didSet string: 12
didSet string: 123
didSet string: 123

Question:

Why?
Why is the didSet closure called twice for each character I type? (I would expect it to be called once for each character.)

Is there anything wrong with the code or is this expected behavior somehow? πŸ€”

Brusa answered 16/1, 2022 at 21:34 Comment(6)
I recall in SwiftUI 1 or 2, it was not called at all. If you have sensitive side-effect then try to use property publisher/combine. – Cajun
Interesting. What do you mean by using a property publisher? Listening to changes via $string.sink {...}? – Brusa
I'm experiencing the same issue and I don't know why this is happening. I know didSet + property wrappers were behaving weird in Swift. Maybe it's a bug? forums.swift.org/t/… – Posy
I have the exact same issue. Even with combine .. .$string.sink {...} the value arrives two times. – Veiled
I am also having this issue, in my case the textfield also queries my database whenever a new character is typed (to autocomplete the entry) so I'm making twice the necessary calls. Has anybody started a ticket with apple, or made a post in the swift forums concerning this? – Butcherbird
Couple of possible solutions/workarounds here: forums.swift.org/t/… – Rahmann
L
19

I’m seeing this issue on Xcode 14.2 RC and iOS 16.2 RC, but weirdly what fixes it is adding a .textFieldStyle(.plain) or .textFieldStyle(.roundedBorder).

I’m really not sure why having no textFieldStyle would affect this, but the binding calls set:{} twice when I have no textFieldStyle set, and as soon as I add one of those, it behaves normally and only calls set:{} once at a time.

I hope this helps someone!

Leges answered 9/12, 2022 at 16:7 Comment(4)
Good catch about .plain "fixing" it... – Ninanincompoop
Wow. This would be funny if it wasn't so ridiculous. – Sit
Still happens on iOS 16.5 – Krumm
Removed this surprising behaviour for me too. However changing focus still triggers an update (using Combine). Solved it by using removeDuplicates() on publisher (https://mcmap.net/q/504733/-swift-combine-publish-twice) – Surly
I
0

There is an operator .removeDuplicates() that you can put before the sink and it will publish only unique values. The problem with .textFieldStyle is that you still get extra calls when the textfield loses focus, or at least on macOS.

Inestimable answered 3/2, 2024 at 14:24 Comment(0)
E
-1
 let binding = Binding<String>(get: {
                textvariable
            }, set: {
                viewModel.setText(query: $0) //add event inside setText
                // do whatever you want here
            })
Eberhardt answered 28/6, 2022 at 9:21 Comment(0)

© 2022 - 2025 β€” McMap. All rights reserved.