Why @Published var didSet is called extra time when it's referenced by TextField/binding?

As of Xcode 13.1 RC, didSet is called a extra time if the var is referenced by binding else where. @State var didSet used to have this same problem, but it seems to be working correctly now.

The problem was worst before Xcode 13.1 RC: it was called two times more!

Is this a bug with SwiftUI or Combine?

import SwiftUI

final class Foo: ObservableObject {
    @Published var number = 0 {
        didSet {
            print(">>> \(#function) didSet is called, number = \(number), oldValue = \(oldValue)")
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var foo: Foo
    @State private var show = false

    var body: some View {
        VStack {
            Text("number = \(foo.number, format: .number)")

            if show {
                // When foo.number is mutated via the Button below,
                // each addition TextField cause another call to didSet!!
                // this same problem used to exist for @State var, but it seems to be fixed as of Xcode 13.1 RC
                TextField("number", value: $foo.number, format: .number, prompt: Text("A prompt"))
                    .textFieldStyle(.roundedBorder)
                    .keyboardType(.numberPad)
                // doing it this way is even worse: it's called two extra times
                TextField("number", value: Binding(get: { foo.number }, set: { foo.number = $0 }), format: .number, prompt: Text("A prompt"))
            }

            // If the TextField above is shown, clicking on this button
            // didSet is called as of Xcode 13.1, it's called twice!, used to be three times!!)
            Button("foo.number Random") {
                foo.number = .random(in: 0...1000)
            }

            Toggle("Show TextField", isOn: $show)
        }
    }
}


@main
struct ObservableObjectPublishedDidSetApp: App {
    @StateObject private var foo = Foo()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(foo)
        }
    }
}

whether this is a bug or not aside,

as a workaround try checking if number != oldValue in number's didSet.

also as an alternative consider using explicit binding (with get + set) instead of didSet.

I did that and it's even worse, it's called two extra time:

 TextField("number", value: Binding(get: { foo.number }, set: { foo.number = $0 }), format: .number, prompt: Text("A prompt"))

edit: I added this form to my original post

I think the problem is with TextField: it somehow mutate the binding when it receive change from the binding. The question, why does it call binding's set?

still, if you put a workaround and check the new value against the old value (in either didSet or Binding method) does that get you where you want?

The problem is the need to trigger re-calculate of dependent values and trigger animations ... what if the new value is actually the same as old? Then oldValue == value is true not because of extra didSet call...I still want animation indicating receive of new data.

i see. in this case another workaround is possible when you make the "same" value different:

struct VersionedInt: Equatable {
    var version: Int = nextVersion() // does some counter++
    let value: Int
}

but indeed this becomes too messy too quick.

there's a valid question what do you do with your animation if there are several real changes in a quick succession. the reasonable thing to do would be to stop the current animation and start the new one. in principle this approach shall handle the situation with those spurious value changes.

1 Like

I just not rely on didSet for now, I can trigger re-calc manually at point of mutate. I just need to keep this in mind if/when I make changes.

I am pretty sure it's some bug in TextField. It should not be setting/muting the binding when bind to value change by something else.

TextEditor doesn't have this problem.

It's very odd that @State var do not have this problem. But using an "explicit bind with get/set closures" has this problem:

// text is an @State var
// This one seem okay, no extra didSet
TextField("Text", text: $text)
// the set closure is called twice, causing didSet called twice!!
TextField("Text", text: Binding(get: { text }, set: { print("šŸ¦†"); text = $0 }))

This is so odd. It's as if TextField is treating the two binding's differently. Maybe they have special case for the binding from an @State var?

Test
import SwiftUI

final class Foo: ObservableObject {
    @Published var number = 0 {
        didSet {
            print("\nšŸ‘‰šŸ‘‰>>> \(#function) didSet is called, number = \(number), oldValue = \(oldValue)")
        }
    }
    @Published var string = "" {
        didSet {
            print("\nšŸ‘‰šŸ‘‰>>> \(#function) didSet is called, string = \(string), oldValue = \(oldValue)")
        }
    }
}

// this same problem used to exist for @State var before, but it seems to be fixed as of Xcode 13.1 RC

struct ContentView: View {
    @EnvironmentObject var foo: Foo
    @State private var text = "" {
        didSet {
            print("\nšŸ‘‰šŸ‘‰>>> \(#function) didSet is called, text = \(text), oldValue = \(oldValue)")
        }
    }
    @State private var show = false

    var body: some View {
        VStack {
            Spacer()

            Group {
                Text("number = \(foo.number, format: .number)")

                if show {
                    // Here in are TextField<F: FormatStyle>
                    // When foo.number is mutated via the Button below,
                    // each addition TextField cause two? extra call to didSet!!
                    TextField("number", value: $foo.number, format: .number, prompt: Text("A prompt"))
                    // set closure is call twice, causing two extra didSet's
                    TextField("number", value: Binding(get: { foo.number }, set: { print("šŸ§"); foo.number = $0 }), format: .number, prompt: Text("A prompt"))
                }

                // If the TextField above is shown, clicking on this button
                // didSet is called one or two extra times as of Xcode 13.1
                Button("foo.number Random") {
                    foo.number = .random(in: 0...1000)
                }
            }

            Divider()

            Group {
                Text("string = \(foo.string)")
                if show {

                    // editing here cause didSet called twice!!
                    TextField("String", text: $foo.string)
                    // didSet called twice!!
                    TextField("String", text: Binding(get: { foo.string }, set: { foo.string = $0 }))
                }
                Button("foo.string") {
                    foo.string += String("ABCDEFGHIJKLMNOPQRSTUVWXYZ".randomElement() ?? "ā‰ļø")
                }
            }

            Divider()

            Group {
                Text("text = \(text)")
                if show {
                    // This one seem okay, no extra didSet
                    TextField("Text", text: $text)
                    // didSet called twice!!
                    TextField("Text", text: Binding(get: { text }, set: { print("šŸ¦†"); text = $0 }))
                    // This is okay, no extra didSet
//                    TextEditor(text: $text)
                }
                Button("foo.string") {
                    text += String("ABCDEFGHIJKLMNOPQRSTUVWXYZ".randomElement() ?? "ā‰ļø")
                }
            }

            Spacer()
            Spacer()

            Toggle("Show TextField's", isOn: $show)
        }
    }
}

The nice thing about SwifUI animations is they're interruptible: when new data arrive, the animation restart anew, my animation is the background "lights up and fade out" in light flash effect: when lots of new data arrive in quick succession, the background light intensity stay high indicating arrival of new data.

Just stumbled on the bug on xcode 13.2.1 :frowning:

1 Like

Yup, pretty frustrating. Still see this on 13.2.1

Came across this bug today. Am using a debounce(delay: 0.5) as a hack-y workaround.

Xcode 14.3.1
Swift 5.8.1