SwiftUI Path Animations
In the fifth of our Thinking in SwiftUI challenges, we asked you to animate a path.
SwiftUI Challenge #5: Path Animations ⌚️
— objc.io (@objcio) March 4, 2020
Animate a path between two starting points, in a motion that doesn't jump between them.
Reply with your solution, and we'll post ours next Tuesday. 👍
Starter code: https://t.co/aSLm1bS49G pic.twitter.com/E8v0onpbdD
We started the challenge with code that draws a line. The starting point changes based on a boolean property, but despite the withAnimation
, the path itself doesn't animate:
let p1 = CGPoint(x: 50, y: 50)
let p2 = CGPoint(x: 100, y: 25)
let p3 = CGPoint(x: 100, y: 100)
struct ContentView: View {
@State var toggle = true
var body: some View {
VStack {
Button("Toggle") {
withAnimation { self.toggle.toggle() }
}
Path { p in
p.move(to: toggle ? p1 : p2)
p.addLine(to: p3)
}.stroke(lineWidth: 2)
}
}
}
To animate a path, we need to tell SwiftUI which properties are animatable. One way to do this is by wrapping the path in a custom Shape
. We create a Line
shape with properties for both the line's start and end points:
struct Line: Shape {
var start, end: CGPoint
func path(in rect: CGRect) -> Path {
Path { p in
p.move(to: start)
p.addLine(to: end)
}
}
}
To animate the start
and end
properties, we need to expose them via animatableData
, and the type of animatableData
needs to conform to the VectorArithmetic
protocol. Unfortunately, CGPoint
does not conform to VectorArithmetic
, and it's bad practice to add this conformance ourselves: you're not supposed to conform types you don't own to protocols you don't own, even though in this case, there wouldn't be much harm in it.
Luckily, CGPoint
does conform to Animatable
, so we can use its animatableData
. We can now make both points of the Line
animatable by creating an animatable pair out of the two CGPoint.Animatable
values:
extension Line {
var animatableData: AnimatablePair<CGPoint.AnimatableData, CGPoint.AnimatableData> {
get { AnimatablePair(start.animatableData, end.animatableData) }
set { (start.animatableData, end.animatableData) = (newValue.first, newValue.second) }
}
}
Our new book, Thinking in SwiftUI, discusses the animation system in more detail in chapter six. You can join the early access here.