0

I would like to make an animation that is like this: a ZStack with a view and over it, a purple rectangle, that initially has scale 0 and opacity 0 and is at the center of screen.

When the animation is triggered, two animations happen:

  1. the rectangle's scale increases from 0 to 1 in 0.4 seconds.
  2. the rectangle's opacity increases from 0 to 1 in 0.3 seconds and from 1 to 0 in 0.1 seconds.

This is me trying to do this:

struct ContentView : View {

  @State private var scale: CGFloat = 0
  @State private var scaleSeed: CGFloat = 0.1
  @State private var counter: Int = 1
  @State private var triggerFlashAnimation = false {
    didSet {
      scale = 0
      counter = 1
    }
  }


  var body: some View {
    
    ZStack {
      Text("Hello")

      if triggerFlashAnimation {
        Rectangle()
          .background(Color.purple)
          .scaleEffect(scale)
          .onAppear {
            scale += scaleSeed
            counter += 1
          }
      }
}

I think this will animate the scale but this is very crude as I don't have any control over time.

I remember back in the day of CoreGraphics that you could define keyframes for time and values.

How do I do that with SwiftUI. I mean a precise animation defining keyframes and values for every parameter?

I googled around and found nothing.

Duck
  • 34,902
  • 47
  • 248
  • 470

1 Answers1

1

SwiftUI doesn't have support for keyframes like we're used to in CoreAnimation (sources: https://stackoverflow.com/a/56908148/560942, https://swiftui-lab.com/swiftui-animations-part1/).

However, you can do a variation on chaining by using delay:

struct ContentView : View {
    
    @State private var scale: CGFloat = 0
    @State private var opacity: Double = 0
    @State private var triggerFlashAnimation = false
    
    func triggerAnimationActions() {
        withAnimation(.linear(duration: 0.4)) {
            scale = 1
        }
        withAnimation(.linear(duration: 0.3)) {
            opacity = 1
        }
        withAnimation(Animation.linear(duration: 0.1).delay(0.3)) {
            opacity = 0
        }
        
        // reset
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            triggerFlashAnimation = false
            scale = 0
            opacity = 0
        }
    }
    
    var body: some View {
        ZStack {
            Text("Hello")
            
            Button(action: { triggerFlashAnimation = true }) {
                Text("Button")
            }.onChange(of: triggerFlashAnimation) { (val) in
                if val {
                    triggerAnimationActions()
                }
            }
            
            if triggerFlashAnimation {
                Rectangle()
                    .fill(Color.purple.opacity(opacity))
                    .frame(width: 100, height: 100)
                    .scaleEffect(scale)
            }
            
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

It's also probably worth looking into AnimatableModifier which lets you define an animatableData property where you can interpolate values, base exact frames on a certain state of the animation, etc, but, as far as I know, doesn't account for timing at all -- rather it just lets you set a state based on the precise value of the current animation. Good resource for reading about AnimatableModifier: https://www.hackingwithswift.com/quick-start/swiftui/how-to-animate-the-size-of-text

jnpdx
  • 45,847
  • 6
  • 64
  • 94