87

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

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
superpuccio
  • 11,674
  • 8
  • 65
  • 93

10 Answers10

181

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)
            }
        }
    }
}
Jenea Vranceanu
  • 4,530
  • 2
  • 18
  • 34
Scott Gribben
  • 1,826
  • 1
  • 5
  • 4
  • you don't need to add zIndex to all of your views because in complex views with many views this is very hard to do. i did it by adding just one zIndex to view that i want to animate and everything works fine. – Sajjad Hajavi Nov 15 '20 at 13:12
98

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)
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Pbk
  • 2,004
  • 14
  • 11
  • 1
    I can confirm this works. It's weird how some implementation on SwiftUI don't work consistently – Jandro Rojas Sep 18 '20 at 15:33
  • 4
    And btw as of right now (iOS 14 / XCode 12) no zIdex is required – Jandro Rojas Sep 18 '20 at 15:35
  • This was the ONLY SOLUTION that worked for me—both `zIndex` and the custom `AnyTransition` syntax were necessary for my app to work. Thank you so so so much @Pbk! – sambecker Sep 19 '20 at 05:02
  • best answer that i have seen. it's no need to zIndex(1) too and worked for me. Thanks alot – Sajjad Hajavi Nov 15 '20 at 11:02
  • The fun thing is that if you chain `.transition(.opacity).animation(.easeInOut(duration: 0.2))` this also doesn't help. – Murlakatam Nov 25 '20 at 07:24
  • 2
    This is actually really funny - if you use `withAnimation()` somewhere else in your code, and for the transition you only use `.opacity`, you get a 1-way animation - only working when moving out of hierarchy. When you use it as this answer tells you, it works both ways. That's kind of a bug isn't it? – iSpain17 Feb 23 '21 at 21:49
  • 2
    wow, has anyone filed a radar for this? transitions in swiftui are so buggy smh – bze12 Mar 27 '21 at 20:49
  • I think it is ok you need to use withAnimation { } or .opacity.animation(.easeInOut) It is a little confusing but make sense. What it interesting .move transtion works without above – Michał Ziobro Sep 08 '21 at 15:55
  • this works for me, but I need to add an id to the View, example: `Text(label) .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) .id("state_" + label)` – GuilhE Apr 12 '22 at 17:38
  • this should be the answer. Is there documentation on this? Seems like a bug. – Xaxxus Mar 06 '23 at 07:10
33

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.

Sajjad Hajavi
  • 501
  • 7
  • 9
12

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.

テッド
  • 776
  • 9
  • 21
10

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)
                }
            }
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
2

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.

2

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
            }
        }
fullmoon
  • 8,030
  • 5
  • 43
  • 58
2

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()
        }
    }
}
Abdullah
  • 231
  • 2
  • 11
0

You should put

 .id(showMessage) 

after the body of your VStack, that should help you.

0

SwiftUI Custom Opacity Transitions

You can make extension for in and out opacity Transitions.

import SwiftUI

extension AnyTransition {
    static var inOpacity: AnyTransition {
        AnyTransition.modifier(
                        active: OpacityModifier(opacity: 0),
                      identity: OpacityModifier(opacity: 1)
        )
    }
    static var outOpacity: AnyTransition {
        AnyTransition.modifier(
                        active: OpacityModifier(opacity: 1),
                      identity: OpacityModifier(opacity: 0)
        )
    }
}
struct OpacityModifier : ViewModifier {
    let opacity: Double
    
    func body(content: Content) -> some View {
        content.opacity(opacity)
    }
}

enter image description here

struct ContentView : View {
    
    @State var isMessageVisible: Bool = false
    @State var text = Text("SwiftUI Transition Animation")
                          .font(.largeTitle)
                          .foregroundColor(.yellow)
    
    var body: some View {
        ZStack {
            Color.indigo.ignoresSafeArea()
            
            VStack {
                Spacer()
                Button("SHOW MY MESSAGE") {
                    withAnimation(.linear(duration: 2)) {
                        isMessageVisible.toggle()
                    }
                }
            }
            if isMessageVisible {
                text.transition(.inOpacity)       // in
            } else {
                text.transition(.outOpacity)      // out
            }
        }
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220