I am playing around with SwiftUI to try understand it better but I can't understand this difference in behavior. In this sample code, why does the struct view able to rotate while the @ViewBuilder function does not rotate?
struct ContentView: View {
@State var isSpinning = false
var body: some View {
StructView()
funcView()
}
@ViewBuilder
func funcView() -> some View {
@State var isSpinning = false
VStack {
Button {
isSpinning.toggle()
}label: {
Text("Spin me")
}
Image(systemName: "arrow.clockwise")
.font(.system(size: 80))
.foregroundColor(.blue)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(.easeInOut(duration: 1), value: isSpinning)
}
.padding()
}
struct StructView: View {
@State var isSpinning = false
var body: some View {
VStack {
Button {
isSpinning.toggle()
}label: {
Text("Spin me")
}
Image(systemName: "arrow.clockwise")
.font(.system(size: 80))
.foregroundColor(.blue)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(.easeInOut(duration: 1), value: isSpinning)
}
.padding()
}
}
}
The fact that it's a @ViewBuilder
function, or the fact that it's a separate function, doesn't really matter to SwiftUI. It is the same as if you inlined funcView
and put it directly in body
.
var body: some View {
StructView()
@State var isSpinning = false
VStack {
Button {
isSpinning.toggle()
}label: {
Text("Spin me")
}
Image(systemName: "arrow.clockwise")
.font(.system(size: 80))
.foregroundColor(.blue)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(.easeInOut(duration: 1), value: isSpinning)
}
.padding()
}
And the problem with this, is that @State
does not work as a local variable. SwiftUI is not designed to track @State
s that are local variables. @State
s should always be declared as instance properties in a View
struct.
In your code, you also did this. You declared @State var isSpinning = false
directly in ContentView
, but you declared it again in funcView
, so references to isSpinning
in funcView
are all resolved to the @State
declared in funcView
.
You should remove the line @State var isSpinning = false
in funcView
, and now references to isSpinning
will resolve to the one declared in ContentView
, and the view will work as expected.
Note that there is a difference between writing everything in body
(calling @ViewBuilder
funds/properties is effectively equivalent to this), and separating view components into different View
structs.
By putting things into a separate View
struct, you effectively create a "barrier" for view updates. For example, StructView.body
won't be called every time something in ContentView
changes, because StructView
does not depend on anything in ContentView
.
On the other hand, funcView
will be called every time something in ContentView
changes, because well, you are literally calling it in ContentView.body
.
So in general, it is better to separate your views into individual View
structs, compared to separating your views into individual @ViewBuilder func
s or @ViewBuilder var
s in the same View
struct. This way, an update at the top of the view hierarchy won't cause everything else to also update.
Here I have added some print
s to demonstrate the difference:
struct ContentView: View {
@State var isSpinning = false
@State var something = "Foo"
var body: some View {
StructView()
funcView()
Button("Change something") {
something = "Bar"
}
Text("Something: \(something)")
}
@ViewBuilder
func funcView() -> some View {
let _ = print("funcView Called")
VStack {
Button {
isSpinning.toggle()
}label: {
Text("Spin me")
}
Image(systemName: "arrow.clockwise")
.font(.system(size: 80))
.foregroundColor(.blue)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(.easeInOut(duration: 1), value: isSpinning)
}
.padding()
}
struct StructView: View {
@State var isSpinning = false
var body: some View {
let _ = print("StructView.body Called")
VStack {
Button {
isSpinning.toggle()
}label: {
Text("Spin me")
}
Image(systemName: "arrow.clockwise")
.font(.system(size: 80))
.foregroundColor(.blue)
.rotationEffect(.degrees(isSpinning ? 360 : 0))
.animation(.easeInOut(duration: 1), value: isSpinning)
}
.padding()
}
}
}