'What does animatableData in SwiftUI do?

I was playing around with the animatableData property for a custom Shape I made but I couldn't really visualise what it does and where the system uses it.

I didn't understand how the system knows which animatableData properties it should interpolate when there's a state change. I also didn't understand what the get part of the animatableData property is used for by the system. The only thing I sort of understand is that SwiftUI will update the animatableData property to all the intermediary values between the original and final value for when an @State variable is changed.

If someone can give a very detailed order of events for the use of animatableData by the system I'll be extremely grateful. Make it as detailed as you can because I'm one of those people who feels scratchy even if I'm not understanding 1% of something (however if I do have any question I'll just ask you in the comments).

Thanks in advance!

P.S. I tried returning a constant in the getter for animatableData and my animation still worked perfectly which has confused me even more. Please let me know what the getter is used for if you can.



Solution 1:[1]

The simplest answer to your question is to override the default animatableData [inherited by the Animatable protocol] with values used to draw your View. Here's an example of how to do that:

    var animatableData: Double {
      get { return percent }
      set { percent = newValue }
   }

Here's an example for you. It:

  • Draws a Ring on the parent View.

As the value of percent [which you hook up when you define animatableData] changes, the animation updates the view by drawing a line along the circumference of the defined circle using the percent value at the time of the update.

animating circle

import SwiftUI

/// This repeats an animation until 5 seconds elapse
struct SimpleAnswer: View {
   /// the start/stop sentinel
   static var shouldAnimate = true
   /// the percentage of the circumference (arc) to draw
   @State var percent = 0.0

   /// animation duration/delay values
   var animationDuration: Double { return 1.0 }
   var animationDelay: Double { return  0.2 }
   var exitAnimationDuration: Double { return 0.3 }
   var finalAnimationDuration: Double { return 1.0 }
   var minAnimationInterval: Double { return 0.1 }

   var body: some View {
      ZStack {
         AnimatingOverlay(percent: percent)
            .stroke(Color.yellow, lineWidth: 8.0)
            .rotationEffect(.degrees(-90))
            .aspectRatio(1, contentMode: .fit)
            .padding(20)
            .onAppear() {
               self.performAnimations()
            }
            .frame(width: 150, height: 150,
                   alignment: .center)
         Spacer()
      }
      .background(Color.blue)
      .edgesIgnoringSafeArea(.all)
   }

   func performAnimations() {
      run()
      if SimpleAnswer.shouldAnimate {
         restartAnimation()
      }
      /// Stop the Animation after 5 seconds
      DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: { SimpleAnswer.shouldAnimate = false })
   }

   func run() {
      withAnimation(.easeIn(duration: animationDuration)) {
         percent = 1
      }
      let deadline: DispatchTime = .now() + animationDuration + animationDelay
      DispatchQueue.main.asyncAfter(deadline: deadline) {
         withAnimation(.easeOut(duration: self.exitAnimationDuration)) {
         }
         withAnimation(.easeOut(duration: self.minAnimationInterval)) {
         }
      }
   }

   func restartAnimation() {
      let deadline: DispatchTime = .now() + 2 * animationDuration + finalAnimationDuration
      DispatchQueue.main.asyncAfter(deadline: deadline) {
         self.percent = 0
         self.performAnimations()
      }
   }
}

/// Draws a Ring on the parent View
/// By default, `Shape` returns the instance of `EmptyAnimatableData` struct as its animatableData.
/// All you have to do is replace this default `EmptyAnimatableData` with a different value.
/// As the value of percent changes, the animation updates the view
struct AnimatingOverlay: Shape {
   var percent: Double

   func path(in rect: CGRect) -> Path {
      let end = percent * 360
      var p = Path()

      p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
               radius: rect.size.width/2,
               startAngle: Angle(degrees: 0),
               endAngle: Angle(degrees: end),
               clockwise: false)
      return p
   }

   /// This example defines `percent` as the value to animate by
   /// overriding the value of `animatableData`
   /// inherited as Animatable.animatableData
   var animatableData: Double {
      get { return percent }
      set { percent = newValue }
   }
}

#if DEBUG
struct SimpleAnswer_Previews : PreviewProvider {
   static var previews: some View {
      SimpleAnswer()
   }
}
#endif

I found these links to help me answer your question. You should find them useful as well.

Wenderlich - How to Create a Splash Screen With SwiftUI

Majid - The Magic of Animatable Values

Animations in SwiftUI - Majid

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1