superpuccio
superpuccio

Reputation: 12972

Transition animation not working properly in SwiftUI

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

Upvotes: 104

Views: 55056

Answers (10)

Sajjad Hajavi
Sajjad Hajavi

Reputation: 640

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 view disappears with animation. To solve this problem you just need to add your view in preview in a VStack. like this :

struct SomeView: 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.

Upvotes: 45

Andy Jazz
Andy Jazz

Reputation: 58053

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
            }
        }
    }
}

Upvotes: 0

Vladimir Sukanica
Vladimir Sukanica

Reputation: 575

You should put

 .id(showMessage) 

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

Upvotes: 0

Scott Gribben
Scott Gribben

Reputation: 2136

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)
            }
        }
    }
}

Upvotes: 212

Abdullah
Abdullah

Reputation: 393

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()
        }
    }
}

Upvotes: 2

Badr Bujbara
Badr Bujbara

Reputation: 8671

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
            }
        }

Upvotes: 2

Pbk
Pbk

Reputation: 2322

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)

Upvotes: 122

UniverseFly
UniverseFly

Reputation: 59

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.

Upvotes: 3

テッド
テッド

Reputation: 906

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.

Upvotes: 14

kontiki
kontiki

Reputation: 40509

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)
                }
            }
        }
    }
}

Upvotes: 11

Related Questions