Search code examples
swiftuiswiftui-animation

How to create asymmetric animation in swiftUI


I'm curious, is there a way to create asymmetric animations in swiftUI? To be clear, I know that you can use .asymmetric to specify different insertion and deletion behaviours for Views that are added and removed. That's not what I'm looking for.

I would like to be able to specify that one Animation curve should be used when applying a change, and a different one should be used when reversing the change.

Here's some code to illustrate:

struct SideFlipper: View {
    @State private var sideFlipped = false
    
    struct GlobeView: View {
        @Binding var sideFlipped: Bool
        
        var body: some View {
            HStack() {
                if sideFlipped { Spacer() }
                Image(systemName: "globe")
                if !sideFlipped { Spacer() }
            }
            .frame(maxWidth: .infinity)
        }
    }
    
    var body: some View {
        VStack {
            Button("Flip Sides") {
                sideFlipped.toggle()
            }
            .padding()
            
            GlobeView(sideFlipped: $sideFlipped)
                .foregroundColor(.red)
                .animation(.easeIn(duration: 1.0), value: sideFlipped)
            
            GlobeView(sideFlipped: $sideFlipped)
                .foregroundColor(.green)
                .animation(.easeOut(duration: 1.0), value: sideFlipped)
        }
        .padding()
    }
}

When you tap the "Flip Sides" button, the red and green globe icons both animate to the right of the screen, but with different animations, because the red icon is using an easeIn animation curve, while the green icon is using easeOut. Tapping the button again sends them back.

I would like to add a third icon that will match the red icon when going to the right, but the green icon when going to the left. In other words, use easeIn when 'sideFlipped is going from false to true, and use easeOut when sideFlipped is going from false to true. How do we achieve this?


Solution

  • While fine tuning the question above, I stumbled upon a possible solution on my own. But since I hadn't been able to find information about this on StackOverflow, I decided to share this info, Q&A style. If anyone has better solutions, please offer them up, but here's what I found:

    You can apply different animation curves based on the State of the view, and the animation curve that's applied at either end-point of the animation will be applied as soon as the animation starts transitioning towards that end-point.

    In this situation, it would mean that when sideFlipped is false, we apply the animation for going from right (.trailing) to left (.leading), ie .easeOut; and when sideFlipped is true, we apply .easeIn. This gives the behaviour I was looking for in the question.

    If I add the following orange GlobeView between the other two, it will give the results I am looking for, matching the red one when going right, and the green one when going left.

                GlobeView(sideFlipped: $sideFlipped)
                    .foregroundColor(.orange)
                    .animation(sideFlipped ? .easeIn(duration: 1.0) : .easeOut(duration: 1.0), value: sideFlipped)
    

    Thankfully, the animation is reasonable if you interrupt the animation by tapping the "Flip Sides" button mid-animation.