A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
@Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text
binding, which would in turn mutate the text
@State var. This would then invalidate the ContentView
, triggering a fresh invocation of body
. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body
only gets hit once, while WrappedText's body
gets run every time the binding changes. And yet, as far as I can tell, the text
state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text
?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text
value (Binding
is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText
it is detected that Text
with new text
is not equal to one with old text
, so body of WrappedText
is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View
struct properties, but we can be involved in it by confirming our view to Equatable
protocol and marking it .equatable()
modifier to give some more complicated logic for detecting if View
should be (or not be) re-rendered.