CGPoint数组实现VectorArithmetic后仅首个点可动画,求问题排查方案
问题:CGPoint数组实现VectorArithmetic后仅首个点动画生效
尝试实现AnimatableCGPointVector遵循VectorArithmetic协议,用于SwiftUI中CGPoint数组的动画,但运行后仅第一个点有动画效果,其余点无响应。
原实现代码
AnimatableCGPointVector 实现
import SwiftUI struct AnimatableCGPointVector: VectorArithmetic { static var zero = AnimatableCGPointVector(values: [.zero]) static func - (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { let values = zip(lhs.values, rhs.values) .map { lhs, rhs in lhs.animatableData - rhs.animatableData } .map { CGPoint(x: $0.first, y: $0.second) } return AnimatableCGPointVector(values: values) } static func -= (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { for i in 0..<min(lhs.values.count, rhs.values.count) { lhs.values[i].animatableData -= rhs.values[i].animatableData } } static func + (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { let values = zip(lhs.values, rhs.values) .map { lhs, rhs in lhs.animatableData + rhs.animatableData } .map { CGPoint(x: $0.first, y: $0.second) } return AnimatableCGPointVector(values: values) } static func += (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { for i in 0..<min(lhs.values.count, rhs.values.count) { lhs.values[i].animatableData += rhs.values[i].animatableData } } var values: [CGPoint] mutating func scale(by rhs: Double) { for i in 0..<values.count { values[i].animatableData.scale(by: rhs) } } var magnitudeSquared: Double { values .map { $0.animatableData.magnitudeSquared } .reduce(0.0, +) } }
视图与Shape实现
struct ContentView4: View { @State var points: AnimatableCGPointVector = .zero var body: some View { VStack(spacing: 24) { GeometryReader { let size = $0.size LineShape( points: points ) .stroke(.orange, style: StrokeStyle(lineWidth: 32, lineCap: .round, lineJoin: .round)) .onAppear { points = AnimatableCGPointVector(values: one(size: size)) } .overlay { HStack { Button(action: { withAnimation(.bouncy(duration: 0.4, extraBounce: 0.3)) { points = AnimatableCGPointVector(values: one(size: size)) } }, label: { Text("one") }) Button(action: { withAnimation(.bouncy(duration: 0.4, extraBounce: 0.3)) { points = AnimatableCGPointVector(values: two(size: size)) } }, label: { Text("two") }) } } } } .frame(width: 320, height: 320) } func one(size: CGSize) -> [CGPoint] { let p1 = CGPoint(x: 90, y: -70) let p2 = CGPoint(x: 160, y: 42) let p3 = CGPoint(x: 75, y: 30) return [p1, p2, p3] } func two(size: CGSize) -> [CGPoint] { let p1 = CGPoint(x: 40, y: 320) let p2 = CGPoint(x: 80, y: 50) let p3 = CGPoint(x: 25, y: 45) return [p1, p2, p3] } } struct LineShape: Shape { var points: AnimatableCGPointVector var animatableData: AnimatableCGPointVector { get { points } set { points = newValue } } func path(in rect: CGRect) -> Path { return Path { p in p.addLines(points.values) } } }
问题根源
问题出在静态属性zero的定义:当前zero是仅包含一个CGPoint.zero的数组,但SwiftUI动画系统需要zero与实际使用的实例保持一致的“维度”(即CGPoint数组的长度)。当动画在3个点的数组间切换时,系统会用这个长度为1的zero做插值计算,导致后续2个点无法被正确映射,最终只有第一个点能执行动画。
另外,原代码中使用min(lhs.values.count, rhs.values.count)处理数组长度不匹配的情况,进一步导致非首个点的数值操作被忽略。
修复方案
1. 修改AnimatableCGPointVector实现
移除静态zero属性,改为动态生成匹配长度的zero实例,并添加长度校验确保向量维度一致:
import SwiftUI struct AnimatableCGPointVector: VectorArithmetic { // 动态生成对应长度的zero实例 static func zero(withCount count: Int) -> AnimatableCGPointVector { AnimatableCGPointVector(values: Array(repeating: .zero, count: count)) } static func - (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { // 开发阶段校验长度一致,快速排查问题 precondition(lhs.values.count == rhs.values.count, "Vector lengths must match") let values = zip(lhs.values, rhs.values) .map { lhs, rhs in lhs.animatableData - rhs.animatableData } .map { CGPoint(x: $0.first, y: $0.second) } return AnimatableCGPointVector(values: values) } static func -= (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { precondition(lhs.values.count == rhs.values.count, "Vector lengths must match") for i in 0..<lhs.values.count { lhs.values[i].animatableData -= rhs.values[i].animatableData } } static func + (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { precondition(lhs.values.count == rhs.values.count, "Vector lengths must match") let values = zip(lhs.values, rhs.values) .map { lhs, rhs in lhs.animatableData + rhs.animatableData } .map { CGPoint(x: $0.first, y: $0.second) } return AnimatableCGPointVector(values: values) } static func += (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { precondition(lhs.values.count == rhs.values.count, "Vector lengths must match") for i in 0..<lhs.values.count { lhs.values[i].animatableData += rhs.values[i].animatableData } } var values: [CGPoint] mutating func scale(by rhs: Double) { for i in 0..<values.count { values[i].animatableData.scale(by: rhs) } } var magnitudeSquared: Double { values .map { $0.animatableData.magnitudeSquared } .reduce(0.0, +) } }
2. 修正ContentView的初始化
确保初始points的长度与后续使用的数组一致:
struct ContentView4: View { // 初始化时用3个点的zero实例,匹配后续数组长度 @State var points: AnimatableCGPointVector = .zero(withCount: 3) // 其余代码保持不变... }
修复原理
VectorArithmetic的核心是将类型视为连续数值向量,每个元素对应向量的一个维度。这里的3个CGPoint对应6个Double维度(x1,y1,x2,y2,x3,y3),zero必须匹配相同维度才能让动画系统正确插值所有点。添加precondition能在开发阶段及时发现长度不匹配的问题,避免隐藏bug。
内容的提问来源于stack exchange,提问作者steve kurogi
相关产品推荐
相关产品推荐

