How can I use Animatable

20 阅读1分钟

Do you know the Animatable Protocol in SwiftUI? It controls the default animations in SwiftUI. If your variables conform to this protocol, they will animate smoothly. For example:

struct Avatar: View {
    
    @State var selected: Bool = false
    
    var body: some View {
        Image(systemName: "heart.fill")
            .resizable()
            .foregroundStyle(.red)
            .frame(width: 48, height: 48)
            .position(x: selected ? 300 : 200, y: 200)
            .animation(.bouncy, value: selected)
            .onTapGesture {
                selected.toggle()
            }
    }
}

when you run this, you'll see a nice animation of the Heart Image, because the position's x and y values conform to the protocol mentioned above.

Now, let's look at another example that does not conform to this protocol:

struct MyPosition: Equatable {
  var x: CGFloat
  var y: CGFloat
}
​
struct MyCircle: View {
  var position: MyPosition
​
  var body: some View {
    GeometryReader { geo in
      Path { path in
        path.addArc(center: CGPoint(x: position.x, y: position.y),
                    radius: min(geo.size.width, geo.size.height) / 2,
                    startAngle: .zero,
                    endAngle: .degrees(360), clockwise: false)
      }
    }
  }
}
​
struct MyAnimatableView: View {
  @State private var position = MyPosition(x: 0, y: 0)
​
  var body: some View {
    MyCircle(position: position)
      .frame(width: 64, height: 64)
      .foregroundColor(.blue)
      .animation(.linear(duration: 1), value: position)
      .onTapGesture {
          position = MyPosition(x: 150, y: 0)
      }
  }
}

If you run this, you will see the MyCircle moves abruptly, and is not smooth.This happends because MyCircle does not conform to the Animatable protocol, so it cannot animate properly. If you want MyCircle to move more smoothly, you can add the following code:

extension MyCircle: Animatable {
    
    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(position.x, position.y) }
        set {
            position.x = newValue.first
            position.y = newValue.second
        }
    }
    
}

By adding this extension, SwiftUI will understand how to animate the position by changing the newValue.

The essence of the animation is that it creates multiple frames and transitions between them based on delta changes. Therefore, we need to let SwiftUI know which parameters can animate.