Search code examples
swiftswiftui

Why does one child view in SwiftUI re-render on parent state change but another doesn't?


The background color of SecondView doesn't change as expected when I push the button, however, the random number in ThirdView updates each time. Why does this happen, both secondView and thirdView have no explicit dependency on the state, I think both of them should not be re-created.

Apple Docs: When the @State value changes, SwiftUI updates the parts of the view hierarchy that depend on the value.

struct ParentView: View {
    @State var counter: Int = 0

    var body: some View {
        VStack {
            Text("ContentView \(counter)")
            SecondView()
            ThirdView()
            Button {
                counter += 1
            } label: {
                Text("Go!")
            }
        }
    }
}

struct SecondView: View {
    var body: some View {
        Text("SecondView")
            .background(.debug)
    }
}

struct ThirdView: View {
  let randomNumber = Int.random(in: 1...100)
  var body: some View {
    Text("Random Number: \(randomNumber)")
  }
}


public extension ShapeStyle where Self == Color {
    static var debug: Color {
        Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
}

Solution

  • If you delete the Text("ContentView \(counter)"), ThirdView will stop updating.

    Do you see now why ThirdView updates? You are requiring the whole VStack to regenerate, so SecondView and ThirdView are candidates to be instantiated all over again; these would be entirely new views, and so the new instance of ThirdView gets a new random number as its randomNumber property. (randomNumber could not possibly change; it is a let constant!)

    As for SecondView, its color comes from a computed variable. It doesn't care what happens to ParentView. So SecondView doesn't need regeneration, and the background color of SecondView remains unchanged — until you run the entire app again.

    To make SecondView act like ThirdView, give it a let property that is a random color, just like ThirdView:

    struct SecondView: View {
        let debug = Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
        var body: some View {
            Text("SecondView")
                .background(debug)
        }
    }
    

    Or, play the game the other way: replace let in ThirdView with @State var. Now ThirdView no longer updates when you tap the button.

    struct ThirdView: View {
        @State var randomNumber = Int.random(in: 1...100)
        var body: some View {
            Text("Random Number: \(randomNumber)")
        }
    }