How does Animatable know which property to animate?

I've been playing with my own custom view and Animatable to make it animate.


I got it going, but do not understand 100% what happens behind the scenes, and this is never good to help understanding something.


Take for example the following simple struct:

struct MyRect: Shape {
  var scale: Double
  
  var animatableData: Double {
    get { return scale }
    set { scale = newValue }
  }
  
  func path(in rect: CGRect) -> Path {
    var path = Path()
    
    let w = min(rect.width, rect.height)*CGFloat(scale)
    let scaledRect = CGRect(origin: rect.origin, size: CGSize(width: w, height: w))
      
    path.addRect(scaledRect)
    
    return path
  }
}


animatableData returns a double, and the getter and setter works with scale. It intuitively makes sense that scale is the property being animated but animatableData does not really have anything to do with scale.


Take for example this slight modification where scale is changed from a double to an Int, where 0 represents 0% and 100 represents 100%:

struct MyRect: Shape {
  var scale: Int
  private var scaleAsDouble: Double
  
  init(scale: Int) {
    self.scale = scale
    scaleAsDouble = Double(scale)
  }
  
  var animatableData: Double {
    get { return scaleAsDouble }
    set { scaleAsDouble = newValue }
  }
  
  func path(in rect: CGRect) -> Path {
    var path = Path()
    
    let w = min(rect.width, rect.height)*CGFloat(scaleAsDouble)/100.0
    let scaledRect = CGRect(origin: rect.origin, size: CGSize(width: w, height: w))
      
    path.addRect(scaledRect)
    
    return path
  }
}


In this case, my Shape as an integer property that cannot be animated, because it cannot have fractional steps. The solution is to store a doulbe that can have a fractional part, and use it as the animatableData.


What I do not get, is how does the framework know to use the scale property? What are the steps that the framework uses when scale is changes from one value to another in order to call the setter on animatableData with the correct values?

Replies

That's the property you define in var animatableData


This should give you more insight:

h ttps://www.hackingwithswift.com/books/ios-swiftui/animating-simple-shapes-with-animatabledata


But I do not see in your code where you call animation ?

Thanks for the response.


I've looked at the link you shared, and it does not give me more insight.


I could be just really confused, or explaining really badly.


animatableData does not return a reference to a property, it only returns a value. As shown in my second example, it uses scaleAsDouble, which is not connected to scale, other than the logic of my code.


What if I had another property? What bugs me, is how the framework knows which property it has to interpolate. I've added prints to the getters and setters of scaleAsDoulbe when changing scale, with a .animation modifier.


What I DO know: The framework knows about the instance of MyRect. When I change the scale property, it creates another instance of MyRect, and init() is called with this new value. Let's say scale was 3 on the first instance, and 4 on the second instance. The framework now knows that it must call the setter on animatableData with the interpolated values between 3 and 4. After calling the setter, it calls path() to get the new interpolated path. But if I had two properties, and both were changed, then how does the framework know that I am animating scale, and not the other property?


What I also did in order to try and understand was to hard-code a value in animatableData that is not the same as the property value. I could see that the framework always calls the getter on animatableData before doing anything else. When the value returned by animatableData was not the same as my property's value, then it would not animate, and the setter would never be called.


From this, it looked to me as if the framework was checking which value is returned by animatableData, and matching that to one of the properties of my struct so that it knew which one to animate, but that does not make sense. If two properties had the same starting value, then this would not work, plus it just sounds silly.


I hope this helps explain what I am trying to figure out. I am sure I am missing something streight forward, but cannot figure it out.


To answer your question about the code that does the animation, this is my test code to animate MyRect:

struct ScaledRectView: View {
  @State private var scale: Int = 100
  let possibleScales: [Int] = [10, 20, 50, 100]
  
  var body: some View {
    VStack {
      MyRect(scale: self.scale)
        .stroke(Color.blue, lineWidth: 4)
        .animation(.easeInOut(duration: 1.0))
      Text(verbatim: "Scale: \(scale) %")
      HStack {
        ForEach(possibleScales, id: \.self) { scale in
          Button(action: {
            self.scale = scale
          }, label: { Text("\(scale) %") })
        }
      }
    }
    .padding()
  }
}

As you can see, this changes the scale as Integers, but the underlying logic (through animatableData) uses a different value that is of type Double.