Animation 基于矢量算法的SwiftUI线图动画

Animation 基于矢量算法的SwiftUI线图动画,animation,graph,swiftui,graphing,Animation,Graph,Swiftui,Graphing,寻找提高绘图性能的方法(希望不使用金属)。将LinearGradient作为填充添加到形状中会对绘图性能产生巨大影响,因此我将其忽略 向量 struct LineGraphVector: VectorArithmetic { var points: [CGPoint.AnimatableData] static func + (lhs: LineGraphVector, rhs: LineGraphVector) -> LineGraphVector {

寻找提高绘图性能的方法(希望不使用金属)。将LinearGradient作为填充添加到形状中会对绘图性能产生巨大影响,因此我将其忽略

向量

struct LineGraphVector: VectorArithmetic {
    var points: [CGPoint.AnimatableData]
    
    static func + (lhs: LineGraphVector, rhs: LineGraphVector) -> LineGraphVector {
        return add(lhs: lhs, rhs: rhs, +)
    }
    
    static func - (lhs: LineGraphVector, rhs: LineGraphVector) -> LineGraphVector {
        return add(lhs: lhs, rhs: rhs, -)
    }
    
    static func add(lhs: LineGraphVector, rhs: LineGraphVector, _ sign: (CGFloat, CGFloat) -> CGFloat) -> LineGraphVector {
        let maxPoints = max(lhs.points.count, rhs.points.count)
        let leftIndices = lhs.points.indices
        let rightIndices = rhs.points.indices
        
        var newPoints: [CGPoint.AnimatableData] = []
        (0 ..< maxPoints).forEach { index in
            if leftIndices.contains(index) && rightIndices.contains(index) {
                // Merge points
                let lhsPoint = lhs.points[index]
                let rhsPoint = rhs.points[index]
                newPoints.append(
                    .init(
                        sign(lhsPoint.first, rhsPoint.first),
                        sign(lhsPoint.second, rhsPoint.second)
                    )
                )
            } else if rightIndices.contains(index), let lastLeftPoint = lhs.points.last {
                // Right side has more points, collapse to last left point
                let rightPoint = rhs.points[index]
                newPoints.append(
                    .init(
                        sign(lastLeftPoint.first, rightPoint.first),
                        sign(lastLeftPoint.second, rightPoint.second)
                    )
                )
            } else if leftIndices.contains(index), let lastPoint = newPoints.last {
                // Left side has more points, collapse to last known point
                let leftPoint = lhs.points[index]
                newPoints.append(
                    .init(
                        sign(lastPoint.first, leftPoint.first),
                        sign(lastPoint.second, leftPoint.second)
                    )
                )
            }
        }
        
        return .init(points: newPoints)
    }
    
    mutating func scale(by rhs: Double) {
        points.indices.forEach { index in
            self.points[index].scale(by: rhs)
        }
    }
    
    var magnitudeSquared: Double {
        return 1.0
    }
    
    static var zero: LineGraphVector {
        return .init(points: [])
    }
}
用法


为什么要避免使用金属?启用它的支持非常简单,只需将
LineGraphShape
s包装到
中,然后使用
drawingGroup()修改即可。试一试:

。。。
团体{
设梯度=线性梯度(
渐变:渐变(颜色:[颜色.红色,颜色.蓝色]),
起点:。领先,
终结点:。尾部
)
线形图形状(点:点[selectedPointType],闭合路径:true)
.填土(坡度)
线形图形状(点:点[selectedPointType],闭合路径:false)
.中风(
坡度
样式:.init(
线宽:4.0,
线头:。圆形,
线条连接:。圆形,
miterLimit:10.0
)
)
}
.drawingGroup()
...

显著提高了性能!我没想到金属可以这么容易实现!谢谢
struct LineGraphShape: Shape {
    var points: [CGPoint]
    let closePath: Bool
    
    init(points: [CGPoint], closePath: Bool) {
        self.points = points
        self.closePath = closePath
    }
    
    var animatableData: LineGraphVector {
        get { .init(points: points.map { CGPoint.AnimatableData($0.x, $0.y) }) }
        set { points = newValue.points.map { CGPoint(x: $0.first, y: $0.second) } }
    }
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: points.first ?? .zero)
            path.addLines(points)
            
            switch (closePath, points.first, points.last) {
            case (true, .some(let firstPoint), .some(let lastPoint)):
                path.addLine(to: .init(x: lastPoint.x, y: rect.height))
                path.addLine(to: .init(x: 0.0, y: rect.height))
                path.addLine(to: .init(x: 0.0, y: firstPoint.y))
                path.closeSubpath()
            default:
                break
            }
        }
    }
}
struct ContentView: View {
    static let firstPoints: [CGPoint] = [
        .init(x: 0.0, y: 0.0),
        .init(x: 20.0, y: 320.0),
        .init(x: 40.0, y: 50.0),
        .init(x: 60.0, y: 10.0),
        .init(x: 90.0, y: 140.0),
        .init(x: 200.0, y: 60.0),
        .init(x: 420.0, y: 20.0),
    ]
    
    static let secondPoints: [CGPoint] = [
        .init(x: 0.0, y: 0.0),
        .init(x: 10.0, y: 200.0),
        .init(x: 20.0, y: 50.0),
        .init(x: 30.0, y: 70.0),
        .init(x: 40.0, y: 90.0),
        .init(x: 50.0, y: 150.0),
        .init(x: 60.0, y: 120.0),
        .init(x: 70.0, y: 20.0),
        .init(x: 80.0, y: 30.0),
        .init(x: 90.0, y: 20.0),
        .init(x: 100.0, y: 0.0),
        .init(x: 110.0, y: 200.0),
        .init(x: 120.0, y: 50.0),
        .init(x: 130.0, y: 70.0),
        .init(x: 140.0, y: 90.0),
        .init(x: 150.0, y: 150.0),
        .init(x: 160.0, y: 120.0),
        .init(x: 170.0, y: 20.0),
        .init(x: 180.0, y: 30.0),
        .init(x: 190.0, y: 20.0),
        .init(x: 200.0, y: 0.0),
        .init(x: 210.0, y: 200.0),
        .init(x: 220.0, y: 50.0),
        .init(x: 230.0, y: 70.0),
        .init(x: 240.0, y: 90.0),
        .init(x: 250.0, y: 150.0),
        .init(x: 260.0, y: 120.0),
        .init(x: 270.0, y: 20.0),
        .init(x: 280.0, y: 30.0),
        .init(x: 290.0, y: 20.0),
        .init(x: 420.0, y: 20.0),
    ]
    
    static let thirdPoints: [CGPoint] = [
        .init(x: 0.0, y: 0.0),
        .init(x: 20.0, y: 30.0),
        .init(x: 40.0, y: 20.0),
        .init(x: 60.0, y: 320.0),
        .init(x: 80.0, y: 200.0),
        .init(x: 100.0, y: 300.0),
        .init(x: 120.0, y: 320.0),
        .init(x: 140.0, y: 400.0),
        .init(x: 160.0, y: 400.0),
        .init(x: 180.0, y: 320),
        .init(x: 200.0, y: 400.0),
        .init(x: 420.0, y: 400.0),
    ]
    
    let pointTypes = [0, 1, 2]
    @State private var selectedPointType = 0
    let points: [[CGPoint]] = [firstPoints, secondPoints, thirdPoints]
    
    var body: some View {
        VStack {
            ZStack {
                LineGraphShape(points: points[selectedPointType], closePath: true)
                    .fill(Color.blue.opacity(0.5))
                
                LineGraphShape(points: points[selectedPointType], closePath: false)
                    .stroke(
                        Color.blue,
                        style: .init(
                            lineWidth: 4.0,
                            lineCap: .round,
                            lineJoin: .round,
                            miterLimit: 10.0
                        )
                    )
                
                Text("\(points[selectedPointType].count) Points")
                    .font(.largeTitle)
                    .animation(.none)
            }
            .animation(.easeInOut(duration: 2.0))
            
            VStack {
                Picker("", selection: $selectedPointType) {
                    ForEach(pointTypes, id: \.self) { pointType in
                        Text("Graph \(pointType)")
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
            }
            .padding(.horizontal, 16.0)
            .padding(.bottom, 32.0)
        }
    }
}