Swift UI 中的动画文本

Animating Text in Swift UI

如何为来自 Swift UI 的 TextTextField 视图设置动画?

我所说的动画是指,当文本发生变化时,它会 "count up"。

例如,给定一些标签,当我将标签文本设置为“100”时,如何创建一个动画,它从 0 上升到 100。我知道这在 UIKit using layers 中是可能的和 CAAnimations,但是在 Swift UI 中使用 .animation() 函数并更改 TextTextField 的文本似乎在动画方面没有做任何事情.

我看过 Animatable 协议及其相关的 animatableData 属性 但它似乎不符合 TextTextField对此。我正在尝试创建一个计数的标签,所以给定一些值,比如 Double 将跟踪对该值的更改,使用 @State@Binding 然后 TextTextField 会将其内容(实际字符串文本)从值设置为动画。

编辑:

为了更清楚,我想重新创建一个标签,在动画时看起来像这样:

您可以在 BindableObject 中使用 CADisplayLink 创建一个计时器,在动画期间更新您的文本。 Gist


class CADisplayLinkBinding: NSObject, BindableObject {

    let didChange = PassthroughSubject<CADisplayLinkBinding, Never>()
    private(set) var progress: Double = 0.0

    private(set) var startTime: CFTimeInterval = 0.0
    private(set) var duration: CFTimeInterval = 0.0
    private(set) lazy var displayLink: CADisplayLink = {
        let link = CADisplayLink(target: self, selector: #selector(tick))
        link.add(to: .main, forMode: .common)
        link.isPaused = true
        return link
    }()

    func run(for duration: CFTimeInterval) {
        let now = CACurrentMediaTime()
        self.progress = 0.0
        self.startTime = now
        self.duration = duration
        self.displayLink.isPaused = false
    }

    @objc private func tick() {
        let elapsed = CACurrentMediaTime() - self.startTime
        self.progress = min(1.0, elapsed / self.duration)
        self.displayLink.isPaused = self.progress >= 1.0
            self.didChange.send(self)
    }

    deinit {
        self.displayLink.invalidate()
    }

}

然后使用它:

@ObjectBinding var displayLink = CADisplayLinkBinding()

var body: some View {
    Text("\(Int(self.displayLink.progress*100))")
        .onAppear {
            self.displayLink.run(for: 10.0)
    }
}

SwiftUI 中有一种纯粹的动画文本方式。这是在 SwiftUI 中使用 AnimatableModifier 协议实现的进度指示器:

我写了一篇内容广泛的文章,记录了 AnimatableModifier(及其错误)的使用。它也包括进度指示器。您可以在这里阅读:https://swiftui-lab.com/swiftui-animations-part3/

struct ContentView: View {
    @State private var percent: CGFloat = 0

    var body: some View {
        VStack {
            Spacer()
            Color.clear.overlay(Indicator(pct: self.percent))

            Spacer()
            HStack(spacing: 10) {
                MyButton(label: "0%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0 } }

                MyButton(label: "27%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 0.27 } }

                MyButton(label: "100%", font: .headline) { withAnimation(.easeInOut(duration: 1.0)) { self.percent = 1.0 } }
            }
        }.navigationBarTitle("Example 10")
    }
}

struct Indicator: View {
    var pct: CGFloat

    var body: some View {
        return Circle()
            .fill(LinearGradient(gradient: Gradient(colors: [.blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing))
            .frame(width: 150, height: 150)
            .modifier(PercentageIndicator(pct: self.pct))
    }
}

struct PercentageIndicator: AnimatableModifier {
    var pct: CGFloat = 0

    var animatableData: CGFloat {
        get { pct }
        set { pct = newValue }
    }

    func body(content: Content) -> some View {
        content
            .overlay(ArcShape(pct: pct).foregroundColor(.red))
            .overlay(LabelView(pct: pct))
    }

    struct ArcShape: Shape {
        let pct: CGFloat

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

            var p = Path()

            p.addArc(center: CGPoint(x: rect.width / 2.0, y:rect.height / 2.0),
                     radius: rect.height / 2.0 + 5.0,
                     startAngle: .degrees(0),
                     endAngle: .degrees(360.0 * Double(pct)), clockwise: false)

            return p.strokedPath(.init(lineWidth: 10, dash: [6, 3], dashPhase: 10))
        }
    }

    struct LabelView: View {
        let pct: CGFloat

        var body: some View {
            Text("\(Int(pct * 100)) %")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.white)
        }
    }
}