How to fix ZStack's views disappear transition not animated in SwiftUI
Table of Contents
The problem: Disappear transition for a view in ZStack not animated
If you animated hiding/showing transition a view in a ZStack, you might experience a disappear transition, not animated from time to time.
To see the problem clearer, I set up a ZStack with a Color as a background and a Text view that appears and disappears based on the isShow
state variable.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
}
}
}
As you can see, the text view is disappearing without animation, but appearing animation work as expected.
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
Cause of the problem
Implicit zIndex
isn't preserve rendering order when a view is removing from the ZStack. When we set isShow
to false
, the Text view is put at the bottom of the ZStack, behind the pink color background. This makes our text view appear to be hiding immediately without animation. We can confirm this behavior by changing our pink background color to half of the screen width.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
GeometryReader { geometry in
Color.pink
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width / 2) // 1
}
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
}
}
}
<1> We set Color to half the size to see the Text view behind it.
You can see that the Text view is still animated (Fading out), but it was doing so behind the pink color view.
Investigation
Since we know that the problem revolves around the z-index, let's set up hypotheses and confirm each one of them. In the end, we will know the conditions that cause the bug. If you are in a hurry, you can jump to the solution.
Hypothesis 1: This bug only occurs when zIndex is not set
To test this, I explicitly set zIndex of all views to zero.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
.zIndex(0)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
.zIndex(0)
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
.zIndex(0)
}
}
}
I got the same problem as when we didn't set z-index, so the cause is not related to whether we implicit or explicit set z-index. So, it might be related to the zIndex
value.
Hypothesis 2: This bug only occurs when zIndex is the same
My next guess is that the problem might come from rendering views with the same zIndex
, so I set them all to one this time.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
.zIndex(1)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
.zIndex(1)
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
.zIndex(1)
}
}
}
The problem is gone, and the transition animation shows correctly, so SwiftUI works just fine even though all views got the same zIndex
, as long as it is not a non-zero value.
Hypothesis 3: What about negative zIndex
To ensure that the problem only occurs on zero zIndex
, let have another experiment with negative zIndex
.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
.zIndex(-1)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
.zIndex(-1)
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
.zIndex(-1)
}
}
}
We set zIndex to -1, and the animation work fine.
Condition that causes the problem
From our hypothesizes, we can conclude that the problem occurs only when the problem view and views below it have zero z-index.
Solution
The problem only occurs when the problem view and views below it have zIndex
of zero.
ZStack uses zIndex to control the front-to-back ordering of views. If views got the same zIndex, ZStack arranges each successive child view a higher z-axis value than the one before it.
To fix the problem, make sure you don't rely on the default ordering behavior of ZStack mentioned above when your z-index is zero.
To solve the problem, explicitly set zIndex
of the problem view or the views below it to any value other than 0.
Set the problem view's z-index to non-zero value
For our example, we only need to set Text view zIndex
to 1
.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
.zIndex(1)
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
}
}
}
We set zIndex of Text to 1 to avoid the rendering problem.
Set the views below the problem view to a non-zero value
Depend on your view layout, set zIndex
for views below the problem view might be easier for you. In this case, we set the Color view to a negative z-index.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
.zIndex(-1)
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
}
}
}
Put the problem view under VStack
If you have a complicated view structure, wrapping the problem view under VStack might be a better choice since you don't have to set zIndex
explicitly. With this approach, text view appearing and disappearing won't affect the rendering order since VStack will always be there.
struct ContentView: View {
@State private var isShow = true
var body: some View {
ZStack {
Color.pink
.edgesIgnoringSafeArea(.all)
VStack { // 1
if isShow {
Text("Hello, SwiftUI!")
.font(.system(size: 56, weight: .heavy))
}
}
VStack {
Spacer()
Button("Show / Hide") {
withAnimation {
isShow.toggle()
}
}.foregroundColor(.black)
}
}
}
}
<1> Put the problem view under VStack, so removing the Text view doesn't change the ZStack rendering order.
You can easily support sarunw.com by checking out this sponsor.
Debug 10x faster with Proxyman: Your ultimate tool to capture HTTPs requests/ responses, natively built for your iPhone and macOS. Special deal for Black Friday: Get 30% off for all Proxyman licenses with code “BLACKFRIDAY2024”.
Conclusion
This problem only happens when the problem view and views below it got the same zero z-index. I have shown you the condition that causes it and some ways to solve the problem. You can pick the right one that suitable for your situation and your view layout.
Read more article about SwiftUI, ZStack, Transition, Animation, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareTuist init: How to use Tuist templates to bootstrap your project
Learn how to use, and limitations of tuist init, a command that bootstrap a new project.
Tuist scaffold: How to use the Tuist template to create a new module for an ongoing project
Learn how the scaffold command helps you to bootstrap new components or features such as a new VIPER module or a new framework for your new feature.