SwiftUI:如何播放一次乒乓动画?向前和向后播放动画的正确方法?

And*_*rew 1 swift swiftui

我需要的样本:

???????  ??? ?????????  ???????????? .

由于没有.onAnimationCompleted { // Some work... }它的相当有问题。

通常,我需要具有以下特征的解决方案:

  1. 播放一些乒乓动画一次的最简短和优雅的方式。不是无限的!
  2. 使代码可重用。例如 - 将其设为 ViewModifier。
  3. 有办法在外部调用动画

我的代码:

import SwiftUI
import Combine

struct ContentView: View {
    @State var descr: String = ""
    @State var onError = PassthroughSubject<Void, Never>()

    var body: some View {
        VStack {
            BlurredTextField(title: "Description", text: $descr, onError: $onError)
            Button("Commit") {
                if self.descr.isEmpty {
                    self.onError.send()
                }
            }
        }
    }
}

struct BlurredTextField: View {
    let title: String
    @Binding var text: String
    @Binding var onError: PassthroughSubject<Void, Never>
    @State private var anim: Bool = false
    @State private var timer: Timer?
    @State private var cancellables: Set<AnyCancellable> = Set()
    private let animationDiration: Double = 1

    var body: some View {
        TextField(title, text: $text)
            .blur(radius: anim ? 10 : 0)
            .animation(.easeInOut(duration: animationDiration))
            .onAppear {
                self.onError
                    .sink(receiveValue: self.toggleError)
                    .store(in: &self.cancellables)
        }
    }

    func toggleError() {
        timer?.invalidate()// no blinking hack
        anim = true
        timer = Timer.scheduledTimer(withTimeInterval: animationDiration, repeats: false) { _ in
            self.anim = false
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Joh*_* M. 7

这个怎么样?不错的呼叫站点,逻辑封装在您的主视图之外,可选的闪烁持续时间。您需要提供的只是PassthroughSubject, 并.send()在您希望眨眼发生时调用。

闪烁演示

import SwiftUI
import Combine

struct ContentView: View {
    let blinkPublisher = PassthroughSubject<Void, Never>()

    var body: some View {
        VStack(spacing: 10) {
            Button("Blink") {
                self.blinkPublisher.send()
            }
            Text("Hi")
                .addOpacityBlinker(subscribedTo: blinkPublisher)
            Text("Hi")
                .addOpacityBlinker(subscribedTo: blinkPublisher, duration: 0.5)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是您要调用的视图扩展

extension View {
    // the generic constraints here tell the compiler to accept any publisher
    //   that sends outputs no value and never errors
    // this could be a PassthroughSubject like above, or we could even set up a TimerPublisher
    //   that publishes on an interval, if we wanted a looping animation
    //   (we'd have to map it's output to Void first)
    func addOpacityBlinker<T: Publisher>(subscribedTo publisher: T, duration: Double = 1)
        -> some View where T.Output == Void, T.Failure == Never {

            // here I take whatever publisher we got and type erase it to AnyPublisher
            //   that just simplifies the type so I don't have to add extra generics below
            self.modifier(OpacityBlinker(subscribedTo: publisher.eraseToAnyPublisher(),
                                         duration: duration))
    }
}
Run Code Online (Sandbox Code Playgroud)

这是ViewModifier魔法真正发生的地方

// you could call the .modifier(OpacityBlinker(...)) on your view directly,
//   but I like the View extension method, as it just feels cleaner to me
struct OpacityBlinker: ViewModifier {
    // this is just here to switch on and off, animating the blur on and off
    @State private var isBlurred = false
    var publisher: AnyPublisher<Void, Never>
    // The total time it takes to blur and unblur
    var duration: Double

    // this initializer is not necessary, but allows us to specify a default value for duration,
    //   and the call side looks nicer with the 'subscribedTo' label
    init(subscribedTo publisher: AnyPublisher<Void, Never>, duration: Double = 1) {
        self.publisher = publisher
        self.duration = duration
    }

    func body(content: Content) -> some View {
        content
            .blur(radius: isBlurred ? 10 : 0)
            // This basically subscribes to the publisher, and triggers the closure
            //   whenever the publisher fires
            .onReceive(publisher) { _ in
                // perform the first half of the animation by changing isBlurred to true
                // this takes place over half the duration
                withAnimation(.linear(duration: self.duration / 2)) {
                    self.isBlurred = true
                    // schedule isBlurred to return to false after half the duration
                    // this means that the end state will return to an unblurred view
                    DispatchQueue.main.asyncAfter(deadline: .now() + self.duration / 2) {
                        withAnimation(.linear(duration: self.duration / 2)) {
                            self.isBlurred = false
                        }
                    }
                }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)