Transition animation not working in SwiftUI

IosSwiftAnimationSwiftui

Ios Problem Overview


I'm trying to create a really simple transition animation that shows/hides a message in the center of the screen by tapping on a button:

struct ContentView: View {
    @State private var showMessage = false
    
    var body: some View {
        ZStack {
            Color.yellow
            
            VStack {
                Spacer()
                Button(action: {
                    withAnimation(.easeOut(duration: 3)) {
                        self.showMessage.toggle()
                    }
                }) {
                    Text("SHOW MESSAGE")
                }
            }
            
            if showMessage {
                Text("HELLO WORLD!")
                    .transition(.opacity)
            }
        }
    }
}

According to the documentation of the .transition(.opacity) animation

> A transition from transparent to opaque on insertion, and from opaque > to transparent on removal.

the message should fade in when the showMessage state property becomes true and fade out when it becomes false. This is not true in my case. The message shows up with a fade animation, but it hides with no animation at all. Any ideas?

EDIT: See the result in the gif below taken from the simulator.

enter image description here

Ios Solutions


Solution 1 - Ios

The problem is that when views come and go in a ZStack, their "zIndex" doesn't stay the same. What is happening is that the when "showMessage" goes from true to false, the VStack with the "Hello World" text is put at the bottom of the stack and the yellow color is immediately drawn over top of it. It is actually fading out but it's doing so behind the yellow color so you can't see it.

To fix it you need to explicitly specify the "zIndex" for each view in the stack so they always stay the same - like so:

struct ContentView: View {
@State private var showMessage = false

var body: some View {
    ZStack {
        Color.yellow.zIndex(0)

        VStack {
            Spacer()
            Button(action: {
                withAnimation(.easeOut(duration: 3)) {
                    self.showMessage.toggle()
                }
            }) {
                Text("SHOW MESSAGE")
            }
        }.zIndex(1)

        if showMessage {
            Text("HELLO WORLD!")
                .transition(.opacity)
                .zIndex(2)
        }
    }
}

}

Solution 2 - Ios

My findings are that opacity transitions don't always work. (yet a slide in combination with an .animation will work..)

.transition(.opacity) //does not always work

If I write it as a custom animation it does work:

.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) 
.zIndex(1)

Solution 3 - Ios

I found a bug in swiftUI_preview for animations. when you use a transition animation in code and want to see that in SwiftUI_preview it will not show animations or just show when some views disappear with animation. for solving this problem you just need to add your view in preview in a VStack. like this :

struct test_UI: View {
    @State var isShowSideBar = false
    var body: some View {
        ZStack {
            Button("ShowMenu") {
                withAnimation {
                    isShowSideBar.toggle()
                }
                
            }
            if isShowSideBar {
                SideBarView()
                    .transition(.slide)
            }
        }
    }
}
struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
           SomeView()
        }
    }
}

after this, all animations will happen.

Solution 4 - Ios

I believe this is a problem with the canvas. I was playing around with transitions this morning and while the don't work on the canvas, they DO seem to work in the simulator. Give that a try. I've reported the bug to Apple.

Solution 5 - Ios

I like Scott Gribben's answer better (see below), but since I cannot delete this one (due to the green check), I'll just leave the original answer untouched. I would argue though, that I do consider it a bug. One would expect the zIndex to be implicitly assigned by the order views appear in code.


To work around it, you may embed the if statement inside a VStack.

struct ContentView: View {
    @State private var showMessage = false

    var body: some View {
        ZStack {
            Color.yellow

            VStack {
                Spacer()
                Button(action: {
                    withAnimation(.easeOut(duration: 3)) {
                        self.showMessage.toggle()
                    }
                }) {
                    Text("SHOW MESSAGE")
                }
            }

            VStack {
                if showMessage {
                    Text("HELLO WORLD!")
                        .transition(.opacity)
                }
            }
        }
    }
}

Solution 6 - Ios

zIndex may cause the animation to be broken when interrupted. Wrap the view you wanna apply transition to in a VStack, HStack or any other container will make sense.

Solution 7 - Ios

I just gave up on .transition. It's just not working. I instead animated the view's offset, much more reliable:

First I create a state variable for offset:

@State private var offset: CGFloat = 200

Second, I set the VStack's offset to it. Then, in its .onAppear(), I change the offset back to 0 with animation:

        VStack{
            Spacer()
            HStack{
                Spacer()
                Image("MyImage")
            }
        }
        .offset(x: offset)
        .onAppear {
            withAnimation(.easeOut(duration: 2.5)) {
                offset = 0
            }
        }

Solution 8 - Ios

Below code should work.

import SwiftUI

struct SwiftUITest: View {
    
    @State private var isAnimated:Bool = false
  
    var body: some View {
        ZStack(alignment:.bottom) {
            VStack{
                Spacer()
                Button("Slide View"){
                    withAnimation(.easeInOut) {
                        isAnimated.toggle()
                    }
                    
                }
                Spacer()
                Spacer()
           
            }
            if isAnimated {
                RoundedRectangle(cornerRadius: 16).frame(height: UIScreen.main.bounds.height/2)
                    .transition(.slide)

            }
            
            
        }.ignoresSafeArea()
    }
}

struct SwiftUITest_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            SwiftUITest()
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmatteopucView Question on Stackoverflow
Solution 1 - IosScott GribbenView Answer on Stackoverflow
Solution 2 - IosPbkView Answer on Stackoverflow
Solution 3 - IosSajjad HajaviView Answer on Stackoverflow
Solution 4 - IosテッドView Answer on Stackoverflow
Solution 5 - IoskontikiView Answer on Stackoverflow
Solution 6 - IosUniverseFlyView Answer on Stackoverflow
Solution 7 - IosfullmoonView Answer on Stackoverflow
Solution 8 - IosAbdullahView Answer on Stackoverflow