1

How to fix ZStack's views disappear transition not animated in SwiftUI

 3 years ago
source link: https://sarunw.com/posts/how-to-fix-zstack-transition-animation-in-swiftui/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

No animation for disappearing transition.No animation for disappearing transition. sponsor-codeshot.png

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.

Text view is still animated. It just does so behind the pink background.Text view is still animated. It just does so behind the pink background.

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.

I suspect that there is something wrong with the underlying implementation when the z-index is zero. I have fired a bug report and will update this article once I know for sure.

sponsor-codeshot.png

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.

The transition animation work as expected.The transition animation work as expected.

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.

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.


You may also like

SwiftUI Animation

Explore how to animate changes in SwiftUI.

SwiftUI
How to create Neumorphic design in SwiftUI

Neumorphism or Neomorphism is a new design trend of UI recently. We are going to see how to implement this in SwiftUI.

SwiftUI

Read more article about SwiftUI, ZStack, Transition, Animation,

or see all available topic

Get new posts weekly

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 — entirely for free.

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

Tweet

Share

Previous
Tuist 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.

Next
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.

← Home


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK