You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 01:12:04