Search code examples
swiftuiswiftui-tabview

SwiftUI Text, changing the text with @State property causing whole view to re-render


I am facing a really strange issue and I have no clue how to solve this problem. There is a Text and a TabView in a VStack. Now the problem is, when I am trying to set the Text's text based on some @State property. SwiftUI refreshes all the views including the pages that are added in the TabView are also recreated. I am sharing an example code snippet that will help it to explain.

My Main View:

struct TestingView: View {
    
    @State private var someText: String = "Hello"
    @State private var page: Int = 0
    
    var body: some View {
        VStack {
            Text(someText + " \(page)")
            
            TabView(selection: $page) {
                ViewOne()
                    .tag(0)
                
                ViewTwo()
                    .tag(1)
                
                ViewThree()
                    .tag(2)
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
        }
    }
}

Tab view pages:

struct ViewOne: View {
    
    init() {
        print("View one init")
    }
    
    var body: some View {
        Text("View one")
    }
}

struct ViewTwo: View {
    
    init() {
        print("View two init")
    }
    
    var body: some View {
        Text("View two")
    }
}

struct ViewThree: View {
    
    init() {
        print("View three init")
    }
    
    var body: some View {
        Text("View three")
    }
}

Now if you I swipe on the TabView it causes change on $page which eventually effects Text . When the text inside Text changes all the views are recreated.

My Questions:

  • What causes SwiftUI internally to behave like this?
  • How can I prevent unintentional redrawing of view?

I have tried making a separate view for the Text and bind the value from TestingView, but the result is same.


Solution

  • ITGuy gave an excellent explanation in their answer. page changes so SwiftUI calls TestingView.body. In there, you call VStack.init, which runs the closure passed to it, so TabView.init gets called, which runs the closure passed to it, so all your tabs' inits get called.

    Unlike UIViews or NSViews (which you might be familiar with), SwiftUI views are very lightweight, and calling a view's init does not do much at all. Each of your tabs has no instance properties, so it is like initialising an empty struct. It is similar to:

    struct Empty { }
    Empty() // surely you'd agree this does almost nothing
    

    What you are returning in body is just a lightweight description for SwiftUI to create the actual views. SwiftUI inspects this description behind the scenes and figures out which views it needs to update. It does this by calling body. Notice that none of your tabs' bodys are ever called except for the first time (try adding a breakpoint to test this!). This shows that they are not updated. TestingView.body is called, because the Text needs updating.

    If you want to avoid the tabs' inits being called, you can wrap the tabs into another View.

    struct ContentView: View {
        @State private var someText: String = "Hello"
        @State private var page: Int = 0
        
        var body: some View {
            VStack {
                Text(someText + " \(page)")
                
                TabView(selection: $page) {
                    Tabs()
                }
                .tabViewStyle(.page(indexDisplayMode: .never))
            }
        }
    }
    
    struct Tabs: View {
        var body: some View {
            ViewOne()
                .tag(0)
            
            ViewTwo()
                .tag(1)
            
            ViewThree()
                .tag(2)
        }
    }
    

    Now, though Tabs.init will be called, ViewOne.init and the other tabs' inits will not be called, because Tabs.body will not be called when page changes.