I know how to animate a single property like StrokeStart/StrokeEnd, or pairs of properties. I’m talking about what you can do from CoreAnimation: Create an animation that animates replacing one path with a different path with the same number of control points. Something like this:
I created that in Swift/UIKit with this project:
https://github.com/DuncanMC/RandomBlobs
That project uses CoreAnimation and creates a CABasicAnimation that builds a CAShapeLayer and replaces the path in the layer with a new path with the same number of control points.
I would expect SwiftUI to support the same ability using its Shape or Path objects, but have not figured out how to do that yet. (Yes, I know I could wrap a UIKit UIView for use in SwiftUI. I haven’t done that yet, but I know it’s possible. I’m looking for a native SwiftUI solution.)
The designated way to do this in SwiftUI is through Animatable
. The idea is you declare some animatableData
, that SwiftUI will set during each frame of the animation, interpolating between the start and end values.
animatableData
needs to be a VectorArithmetic
type. For this example, let’s suppose the shape has 5 points, then animatableData
would be representing a vector of 10 values. There is a built-in AnimatablePair
that allows you to compose two VectorArithmetic
s together, but for this example I’ll just handwrite such a type:
struct FivePoints: VectorArithmetic, AdditiveArithmetic {
// this random function is for later...
static func random() -> FivePoints {
.init(p1: .random(), p2: .random(), p3: .random(), p4: .random(), p5: .random())
}
static func -(lhs: FivePoints, rhs: FivePoints) -> FivePoints {
.init(
p1: lhs.p1 - rhs.p1,
p2: lhs.p2 - rhs.p2,
p3: lhs.p3 - rhs.p3,
p4: lhs.p4 - rhs.p4,
p5: lhs.p5 - rhs.p5
)
}
static func +(lhs: FivePoints, rhs: FivePoints) -> FivePoints {
.init(
p1: lhs.p1 + rhs.p1,
p2: lhs.p2 + rhs.p2,
p3: lhs.p3 + rhs.p3,
p4: lhs.p4 + rhs.p4,
p5: lhs.p5 + rhs.p5
)
}
var p1: CGPoint
var p2: CGPoint
var p3: CGPoint
var p4: CGPoint
var p5: CGPoint
public mutating func scale(by rhs: Double) {
p1.x *= rhs
p1.y *= rhs
p2.x *= rhs
p2.y *= rhs
p3.x *= rhs
p3.y *= rhs
p4.x *= rhs
p4.y *= rhs
p5.x *= rhs
p5.y *= rhs
}
public var magnitudeSquared: Double {
pow(p1.x, 2) + pow(p2.x, 2) + pow(p3.x, 2) + pow(p4.x, 2) + pow(p5.x, 2) +
pow(p1.y, 2) + pow(p2.y, 2) + pow(p3.y, 2) + pow(p4.y, 2) + pow(p5.y, 2)
}
public static var zero: FivePoints {
.init(p1: .zero, p2: .zero, p3: .zero, p4: .zero, p5: .zero)
}
}
func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
extension CGPoint {
static func random() -> CGPoint {
.init(x: Double.random(in: 0..<300), y: Double.random(in: 0..<300))
}
}
The declaration of FivePoints
is a bit tedious to write, because I can’t think of how you would implement a general n-dimensional vector type in Swift. In practice, you could probably generate this with a macro.
Then you can write such a View
:
struct FivePointShape: View, Animatable {
var points: FivePoints
var animatableData: FivePoints {
get { points }
set { points = newValue }
}
var body: some View {
Path { p in
p.addLines([points.p1, points.p2, points.p3, points.p4, points.p5])
p.closeSubpath()
}
.fill(.yellow)
}
}
This could have been a Shape
as well, if the points are relative to some other CGRect
(recall the CGRect
parameter of Shape.path(in:)
). In this example I’ll just keep it simple and say that the CGPoint
s are absolute.
Usage:
struct ContentView: View {
@State var points = FivePoints.random()
var body: some View {
ZStack {
FivePointShape(points: points)
Button("Animate!") {
withAnimation {
points = .random()
}
}
}
}
}
Again, I’ve kept it simple here and just randomly choose a set of points to animate to, so most of the times the path intersects itself. In any case, you can see that each point animates as desired.
6