How to get bounce animation in SwiftUI?

Viewed 4262

How do I achieve the bounce animation similar to the one on macOS dock when an application needs attention and it bounces on the dock. SwiftUI seems to only have ease-curves and spring, which don't really emphasize the way a bounce does. I've attempted various spring animations and combinations of ease curves and timingCurves to try and get the bounce animation working but nothing really did the job.

The closest to a bounce animation is interpolating spring but the main problem with these animations is that they overshoot during the animation, whereas bounce animations don't.

struct ContentView: View {
    @State var bounce = false
    @State private var initialVelocity:Double = 1
    @State private var damping:Double = 1
    @State private var stiffness:Double = 1
    var body: some View {
        VStack {
            Circle().fill(Color.red).frame(width:50,height:50)
            .offset(y: bounce ? 0 : -80)
                .animation(.interpolatingSpring(stiffness: self.stiffness, damping: self.damping, initialVelocity: self.initialVelocity))

            HStack(){
                Text("stiffness")
                Slider(value: $stiffness, in: 0...100)
            }
            HStack(){
                Text("damping")
                Slider(value: $damping, in: 0...100)
            }
            HStack(){
                Text("initialVelocity")
                Slider(value: $initialVelocity, in: 0...100)
            }
            Button("Animate" ){
                self.bounce.toggle()
            }
        }.padding(.horizontal)
    }
}

What I'm looking for is a bounce animation that replicates gravity, it's a pretty common animation that is available in a lot of games and software enter image description here

2 Answers

What's about .interpolatingSpring(...)?
Consider the following example but keep in mind, that you might have to play around with the values for stiffness, etc.:

struct ContentView: View {
    @State var test = false
    
    var body: some View {
        VStack {
            Text("Animatable")
            .offset(y: test ? 0 : -80)
            .animation(.interpolatingSpring(stiffness: 350, damping: 5, initialVelocity: 10))
            
            Button(action: {self.test.toggle()}) {
                Text("Animate")
            }
        }
    }
}

Using multiple offsets, delays and easing I was able to get fairly close to replicating that specific animation.

Demo

struct ContentView: View {

@State var bounceHeight: BounceHeight? = nil

func bounceAnimation() {
    withAnimation(Animation.easeOut(duration: 0.3).delay(0)) {
        bounceHeight = .up100
    }
    withAnimation(Animation.easeInOut(duration: 0.04).delay(0)) {
        bounceHeight = .up100
    }
    withAnimation(Animation.easeIn(duration: 0.3).delay(0.34)) {
        bounceHeight = .base
    }
    withAnimation(Animation.easeOut(duration: 0.2).delay(0.64)) {
        bounceHeight = .up40
    }
    withAnimation(Animation.easeIn(duration: 0.2).delay(0.84)) {
        bounceHeight = .base
    }
    withAnimation(Animation.easeOut(duration: 0.1).delay(1.04)) {
        bounceHeight = .up10
    }
    withAnimation(Animation.easeIn(duration: 0.1).delay(1.14)) {
        bounceHeight = .none
    }
}

var body: some View {
    VStack {
        Text("☝️")
            .font(.system(size: 200))
            .multilineTextAlignment(.center)
            .minimumScaleFactor(0.2)
            .lineLimit(1)
    }
    .padding(8)
    .frame(width: 72, height: 72)
    .background(.purple.opacity(0.4))
    .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
    .shadow(radius: 3)
    .overlay(
        RoundedRectangle(cornerRadius: 16)
            .stroke(.purple, lineWidth: 2)
    )
    .offset(y: bounceHeight?.associatedOffset ?? 0)
    .onTapGesture {
        bounceAnimation()
    }
}
}

enum BounceHeight {
case up100, up40, up10, base
var associatedOffset: Double {
    switch self {
    case .up100:
        return -100
    case .up40:
        return -40
    case .up10:
        return -10
    case .base:
        return 0
    }
}
}
Related