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)
}
}
}