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.)
1 Answer1
The designated way to do this in SwiftUI is throughAnimatable. The idea is you declare someanimatableData, that SwiftUI will set during each frame of the animation, interpolating between the start and end values.
animatableData needs to be aVectorArithmetic type. For this example, let's suppose the shape has 5 points, thenanimatableData would be representing a vector of 10 values. There is a built-inAnimatablePair that allows you to compose twoVectorArithmetics together, but that's only composing two at a time.
Here is a type that can compose any number ofVectorArithmetic types together
struct ComposedVectorArithmetic<each V>: VectorArithmetic where repeat each V: VectorArithmetic { var elements: (repeat each V) static func +(lhs: Self, rhs: Self) -> Self { .init(elements: (repeat (each lhs.elements) + (each rhs.elements))) } static func -(lhs: Self, rhs: Self) -> Self { .init(elements: (repeat (each lhs.elements) - (each rhs.elements))) } mutating func scale(by rhs: Double) { elements = (repeat (each elements).scaled(by: rhs)) } var magnitudeSquared: Double { var sum = 0.0 for element in repeat each elements { sum += element.magnitudeSquared } return sum } static func ==(lhs: Self, rhs: Self) -> Bool { for (a, b) in repeat (each lhs.elements, each rhs.elements) { if a != b { return false } } return true } static var zero: Self { .init(elements: (repeat (each V).zero)) }}extension ComposedVectorArithmetic: Sendable where repeat each V: Sendable {}Then you can write such aShape:
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) }}struct FivePointShape: Shape, Animatable { typealias AnimatableData = ComposedVectorArithmetic<UnitPoint,UnitPoint,UnitPoint,UnitPoint,UnitPoint> var points: AnimatableData var animatableData: AnimatableData { get { points } set { points = newValue } } func path(in rect: CGRect) -> Path { Path { p in p.addLines([ points.elements.0.point(in: rect), points.elements.1.point(in: rect), points.elements.2.point(in: rect), points.elements.3.point(in: rect), points.elements.4.point(in: rect), ]) p.closeSubpath() } }}extension UnitPoint: @retroactive AdditiveArithmetic, @retroactive VectorArithmetic { public mutating func scale(by rhs: Double) { x *= rhs y *= rhs } public var magnitudeSquared: Double { pow(x, 2) + pow(y, 2) } public static func +(lhs: UnitPoint, rhs: UnitPoint) -> UnitPoint { UnitPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } public static func -(lhs: UnitPoint, rhs: UnitPoint) -> UnitPoint { UnitPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } // this is for later... static func random() -> UnitPoint { .init(x: .random(in: 0...1), y: .random(in: 0...1)) } func point(in rect: CGRect) -> CGPoint { .init(x: rect.minX + x * rect.width, y: rect.minY + y * rect.height) }}Usage:
struct ContentView: View { @State var points = FivePointShape.AnimatableData.zero var body: some View { ZStack { FivePointShape(points: points) Button("Animate!") { withAnimation { points.elements = ( .random(), .random(), .random(), .random(), .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.
To do curves, you can create a type that represents a curve, and compose that usingComposedVectorArithmetic.
struct UnitBezier: VectorArithmetic { var points: ComposedVectorArithmetic<UnitPoint, UnitPoint, UnitPoint> var control1: UnitPoint { get { points.elements.0 } set { points.elements.0 = newValue } } var control2: UnitPoint { get { points.elements.1 } set { points.elements.1 = newValue } } var endPoint: UnitPoint { get { points.elements.2 } set { // for some reason swiftc cannot infer the type of 'points.elements' here, and crashes // so I have to write it in this ugly way... var elems: (UnitPoint, UnitPoint, UnitPoint) = points.elements elems.2 = newValue points.elements = elems } } init(control1: UnitPoint, control2: UnitPoint, endPoint: UnitPoint) { points = .init(elements: (control1, control2, endPoint)) } init(points: ComposedVectorArithmetic<UnitPoint, UnitPoint, UnitPoint>) { self.points = points } mutating func scale(by rhs: Double) { points.scale(by: rhs) } var magnitudeSquared: Double { points.magnitudeSquared } static var zero: UnitBezier { .init(control1: .zero, control2: .zero, endPoint: .zero) } static func +(lhs: Self, rhs: Self) -> Self { .init(points: lhs.points + rhs.points) } static func -(lhs: Self, rhs: Self) -> Self { .init(points: lhs.points - rhs.points) } }// example:struct ThreeCurves: Shape, Animatable { typealias AnimatableData = ComposedVectorArithmetic<UnitPoint, UnitBezier, UnitBezier, UnitBezier> var points: AnimatableData var animatableData: AnimatableData { get { points } set { points = newValue } } func path(in rect: CGRect) -> Path { Path { p in p.move(to: points.elements.0.point(in: rect)) p.addCurve( to: points.elements.1.endPoint.point(in: rect), control1: points.elements.1.control1.point(in: rect), control2: points.elements.1.control2.point(in: rect) ) p.addCurve( to: points.elements.2.endPoint.point(in: rect), control1: points.elements.2.control1.point(in: rect), control2: points.elements.2.control2.point(in: rect) ) p.addCurve( to: points.elements.3.endPoint.point(in: rect), control1: points.elements.3.control1.point(in: rect), control2: points.elements.3.control2.point(in: rect) ) p.closeSubpath() } }}12 Comments
Explore related questions
See similar questions with these tags.

