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:
I have tried making a separate view for the Text
and bind the value from TestingView
, but the result is same.
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' init
s get called.
Unlike UIView
s or NSView
s (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' body
s 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' init
s 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' init
s will not be called, because Tabs.body
will not be called when page
changes.