Search code examples
swiftuiproperty-wrapper

How to force SwiftUI view to reinitialize but not recompute(rerender) body


Body will recompute(rerender) only when source of truth changes far as I know.

Question 1: Will changing of source of truth ALWAYS trigger reinitialization and rerender combined?

Question 2: Is it possible to just reinit without rerendering SwiftUI view and if so how does recomputation not happening when we reinitialized view?

Note: rerendering and recomputing body is same thing as I know.


Solution

  • Will changing of source of truth ALWAYS trigger reinitialization and rerender combined?

    No. Changing a source of truth causes the bodys of views that depend on that source of truth to be re-evaluated, but those views' inits are not necessarily called.

    Calling init is something that happens in your own code - you can literally see where it occurs in your code - whereas body is called by SwiftUI internals, whenever SwiftUI needs to.

    Example:

    @main
    struct FooApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
            
        }
    }
    
    struct ContentView: View {
        @State var x = 0
        
        init() {
            print("Init!")
        }
        
        var body: some View {
            let _ = print("Body!")
            Button("\(x)") {
                x += 1
            }
        }
    }
    

    Pressing the button changes x, and ContentView.body is evaluated.

    "Init!" will not be printed when you press the button, because you can see that ContentView.init is only called in FooApp.body, but FooApp.body is not called. This is because FooApp does not depend on x (in fact it doesn't know about x at all), and so doesn't need to have its body re-evaluated.

    Of course Button.init will also be called, because that is exactly what you are doing in ContentView.body. If that is what you mean, then yes, the init of at least one view will be called when body is re-evaluated.


    Is it possible to just reinit without rerendering SwiftUI view?

    Yes. Suppose a source of truth changes, and a view's body is re-evaluated, where some other view's init is called. However, that view doesn't depend on the source of truth that changed. In this case that view's body will not be re-evaluated.

    Consider a view without any dependencies:

    struct ContentView: View {
        @State var x = 0
        
        var body: some View {
            NoDependencies()
            
            Button("\(x)") {
                x += 1
            }
        }
    }
    
    struct NoDependencies: View {
        init() {
            print("Init!")
        }
        
        var body: some View {
            let _ = print("Body!")
            Text("\(Int.random(in: 0..<10))")
        }
    }
    

    When I press the button, NoDependencies.init is called (because ContentView.body is re-evaluated), but NoDependencies.body is not called.

    how does recomputation not happening when we reinitialized view?

    SwiftUI simply compares the existing view before the change in source of truth, with the view you created after the change in source of truth. If these are different, that view's body is re-evaluated.

    I don't know the details of how SwiftUI does this when the properties of the view are not Equatable (compares the raw bytes perhaps?), but if they are Equatable, SwiftUI just uses the == operator.

    In fact, you can make your view conform to Equatable to control exactly when a view should re-evaluate body.

    Example:

    struct ContentView: View {
        @State var x = 0
        @State var y = 0
        
        var body: some View {
            VStack {
                Button("Change X") {
                    x += 1
                }
                Button("Change Y") {
                    y += 1
                }
                SomeView(x: $x, y: $y)
                    .equatable()
            }
        }
    }
    
    struct SomeView: View {
        let x: Int
        let y: Int
    
        // adding a @State here or else SwiftUI would directly compare x and y
        @State var z = 0
        
        var body: some View {
            Text("\(x) \(y)")
        }
    }
    
    // this Equatable implementation only compares x
    extension SomeView: Equatable {
        static func ==(lhs: SomeView, rhs: SomeView) -> Bool {
            return lhs.x == rhs.x
        }
    }
    

    Notice that pressing the "Change Y" button will not update SomeView, because == only compares x.