Search code examples
swiftswiftui

How does this SwiftUI binding + state example manage to work without re-invoking body?


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?


Solution

  • 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.