如何在 SwiftUI 中设置动画路径

How to Animate Path in SwiftUI

不熟悉 SwiftUI,而且关于这个新框架的文档还不多。我想知道是否有人熟悉如何在 SwiftUI 中为 Path 设置动画。

例如给定一个视图,让我们说这个简单的 RingView

struct RingView : View {   
    var body: some View {
        GeometryReader { geometry in
            Group {
                // create outer ring path
                Path { path in
                    path.addArc(center: center,
                                radius: outerRadius,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 360),
                                clockwise: true)
                }
                .stroke(Color.blue)

                // create inner ring
                Path { path in
                    path.addArc(center: center,
                                radius: outerRadius,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 180),
                                clockwise: true)
                }
                .stroke(Color.red)
                .animation(.basic(duration: 2, curve: .linear))
            }
        }
        .aspectRatio(1, contentMode: .fit)
    }
}

显示的是:

现在我想知道如何为内环设置动画,即蓝线内的红线。我想要做的动画是一个简单的动画,其中路径从开始出现并遍历到结束。

这使用 CoreGraphics 和旧的 UIKit 框架相当简单,但它似乎并不像向内部路径添加一个简单的 .animation(.basic(duration: 2, curve: .linear)) 并使用 withAnimation 块显示视图做任何事情。

我查看了 SwiftUI 上提供的 Apple 教程,但它们实际上只涵盖了更多 in-depth 视图(例如 Image 上的 move/scale 动画。

有关于如何在 SwiftUI 中为 PathShape 设置动画的指南吗?

WWDC session 237 (Building Custom Views with SwiftUI) 中展示了路径动画。关键是使用 AnimateData。您可以跳到 31:23,但我建议您至少在 27:47.

分钟开始

您还需要下载示例代码,因为为了方便,演示文稿中没有显示(也没有解释)有趣的部分:https://developer.apple.com/documentation/swiftui/drawing_and_animation/building_custom_views_in_swiftui


更多文档: 由于我最初发布了答案,所以我继续研究如何为 Paths 设置动画并发布了一篇文章,其中对 Animatable 协议以及如何将其与 Paths 一起使用进行了广泛的解释:https://swiftui-lab.com/swiftui-animations-part1/


更新:

我一直在研究形状路径动画。这是一个 GIF。

代码如下:

重要提示:代码不会在 Xcode 实时预览上设置动画。它需要在模拟器或真实设备上 运行。

import SwiftUI

struct ContentView : View {
    var body: some View {
        RingSpinner().padding(20)
    }
}

struct RingSpinner : View {
    @State var pct: Double = 0.0

    var animation: Animation {
        Animation.basic(duration: 1.5).repeatForever(autoreverses: false)
    }

    var body: some View {

        GeometryReader { geometry in
            ZStack {
                Path { path in

                    path.addArc(center: CGPoint(x: geometry.size.width/2, y: geometry.size.width/2),
                                radius: geometry.size.width/2,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 360),
                                clockwise: true)
                }
                .stroke(Color.green, lineWidth: 40)

                InnerRing(pct: self.pct).stroke(Color.yellow, lineWidth: 20)
            }
        }
        .aspectRatio(1, contentMode: .fit)
            .padding(20)
            .onAppear() {
                withAnimation(self.animation) {
                    self.pct = 1.0
                }
        }
    }

}

struct InnerRing : Shape {
    var lagAmmount = 0.35
    var pct: Double

    func path(in rect: CGRect) -> Path {

        let end = pct * 360
        var start: Double

        if pct > (1 - lagAmmount) {
            start = 360 * (2 * pct - 1.0)
        } else if pct > lagAmmount {
            start = 360 * (pct - lagAmmount)
        } else {
            start = 0
        }

        var p = Path()

        p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
                 radius: rect.size.width/2,
                 startAngle: Angle(degrees: start),
                 endAngle: Angle(degrees: end),
                 clockwise: false)

        return p
    }

    var animatableData: Double {
        get { return pct }
        set { pct = newValue }
    }
}