在 SwiftUI 的动画期间有条件地更新第二个 属性

Conditionally updating a second property during animation in SwiftUI

我在 SwiftUI 中绘制了一个动画 Arc,表示来自两个设备的数据。当弧向左移动时,“仪表”表示左侧设备的数据高于右侧,反之亦然。 “顶”为零,绘制 Arc 形状时为 270 度。正因为如此,我在 clockwise 属性 上设置了一个条件,这样动画就会出现在零 (270) 的左侧或右侧:

var clockwise: Bool {
    get { endAngle.degrees > 270 ? false : true }
}

当 属性 endAngle 从小于 270 变为大于 270 时,或相反,圆弧未正确绘制并显示为圆形,因为顺时针不是将动画的一部分通过 270 设置为新的 endAngle.

有没有办法延迟 clockwise 的变化,直到动画经过 270 度?

我在下面的视图中添加了一些注释代码。为了动画 Angle,它必须符合 VectorArithmetic 这就是扩展的原因。

struct AverageGauge: View {
    
    @State var endAngle = Angle(degrees: 271.0)
    
    // Property I'd like to update during the animation
    var clockwise: Bool {
        get { endAngle.degrees > 270 ? false : true }
    }
    
    var body: some View {
        VStack {
            Arc(startAngle: .degrees(270), endAngle: endAngle,
                clockwise: clockwise)
                .stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
                .frame(width: 100, height: 100)
                // Tap gesture simulates changing data
                .onTapGesture {
                    withAnimation(Animation.easeIn(duration: 2.0)) {
                     // endAngle animated here
                        endAngle = Angle(degrees: Double.random(in: 180...360))
                    }
            }
            Text(String(describing: endAngle.degrees))
            Text("\(String(clockwise))")
        }
    }
}

struct Arc: Shape {
    
    var startAngle: Angle
    var endAngle: Angle
    var clockwise: Bool
    
//    var startAngleAnimatable: Angle {
//        get { startAngle }
//        set {startAngle = Angle(degrees: 270.0) }
//    }
    
    // Required to animate endAngle
    var animatableData: Angle {
        get { endAngle }
        set { endAngle = newValue }
    }
    
//    var clockwiseAnimatable: Bool {
//        get { clockwise }
//        set { clockwise = newValue }
//    }
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)

        return path
    }
}

extension Angle: VectorArithmetic {
    
    public static var zero = Angle(degrees: 0.0)
    
    public static func + (lhs: Angle, rhs: Angle) -> Angle {
        Angle(degrees: lhs.degrees + rhs.degrees)
    }
    
    public static func - (lhs: Angle, rhs: Angle) -> Angle {
        Angle(degrees: lhs.degrees - rhs.degrees)
    }
    
    public static func += (lhs: inout Angle, rhs: Angle) {
        lhs = Angle(degrees: lhs.degrees + rhs.degrees)
    }
    
    public static func -= (lhs: inout Angle, rhs: Angle) {
        lhs = Angle(degrees: lhs.degrees - rhs.degrees)
    }
    
    public mutating func scale(by rhs: Double) {
        self.degrees = self.degrees * rhs
    }
    
    public var magnitudeSquared: Double {
        get { 0.0 }
    }
}

// Preview in case you want to paste it. 
struct AverageGauge_Previews: PreviewProvider {
    static var previews: some View {
        AverageGauge()
    }
}

这是动画的 gif。可以看到当新值和270在同一边的时候看起来很正常,但是当动画遍历零(270)的时候就出现了一个圆,因为clockwise设置错误

我用修剪过的圆简化了它,导致了相同的结果。
如果值从 + 更改为 - 或相反,我会等待第一个动画完成后再开始第二个动画。

struct ContentView: View {
    
    @State var gaugeValue: Double = 0.8 // now in values -1 (-45°) to +1 (+45°)
    
    var body: some View {
        VStack {
            let fraction: CGFloat = abs(gaugeValue) * 0.25
            let rotation = gaugeValue < 0 ? (gaugeValue * 90) - 90 : -90
            Circle()
                .trim(from: 0, to: fraction )
                .rotation(Angle(degrees: rotation), anchor: .center)
            
                .stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
                .frame(width: 100, height: 100)
            
            
            // Tap gesture simulates changing data
            Button {
                let newGaugeValue = CGFloat.random(in: -1 ... 1)
                
                if newGaugeValue * gaugeValue < 0 { // if change of +/-
                    withAnimation(Animation.easeOut(duration: 1.0)) {
                        gaugeValue = 0
                    }
                    // delay for reaching 0 point
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                        withAnimation(Animation.easeIn(duration: 1.0)) {
                            gaugeValue = newGaugeValue
                        }
                    }
                } else { // no change of +/-
                    withAnimation(Animation.easeIn(duration: 1.0)) {
                        gaugeValue = newGaugeValue
                    }
                }
            } label: {
                Text("New Data")
            }
            
            Text("\(gaugeValue)")
        }
    }
}