延迟SwiftUI中的重复动画,并在两个完整的自动翻转重复周期之间执行

延迟SwiftUI中的重复动画,并在两个完整的自动翻转重复周期之间执行,swift,swiftui,Swift,Swiftui,我正在用SwiftUI构建一个Apple Watch应用程序,它可以读取用户的心率,并将其显示在心脏符号旁边 我有一个动画,使心脏符号反复跳动。因为我知道实际用户的心率,所以我想让它以与用户心率相同的频率跳动,每次心率变化时更新动画 我可以通过将心率除以60来确定两次心跳之间的间隔时间。例如,如果用户的心率为80 BPM,则动画应每0.75秒(60/80)发生一次 下面是我现在的示例代码,其中currentBPM是一个常量,但通常会被更新 struct SimpleBeatingView: Vi

我正在用SwiftUI构建一个Apple Watch应用程序,它可以读取用户的心率,并将其显示在心脏符号旁边

我有一个动画,使心脏符号反复跳动。因为我知道实际用户的心率,所以我想让它以与用户心率相同的频率跳动,每次心率变化时更新动画

我可以通过将心率除以60来确定两次心跳之间的间隔时间。例如,如果用户的心率为80 BPM,则动画应每0.75秒(60/80)发生一次

下面是我现在的示例代码,其中
currentBPM
是一个常量,但通常会被更新

struct SimpleBeatingView: View {
    
    // Once I get it working, this will come from a @Published Int that gets updated any time a new reading is avaliable.
    let currentBPM: Int = 80
    
    @State private var isBeating = false
    
    private let maxScale: CGFloat = 0.8
    
    var beatingAnimation: Animation {
        
        // The length of one beat
        let beatLength = 60 / Double(currentBPM)
        
        return Animation
            .easeInOut(duration: beatLength)
            .repeatForever()
    }
    
    var body: some View {
        Image(systemName: "heart.fill")
            .font(.largeTitle)
            .foregroundColor(.red)
            .scaleEffect(isBeating ? 1 : maxScale)
            .animation(beatingAnimation)
            .onAppear {
                self.isBeating = true
            }
    }
}
我想让这个动画表现得更像苹果的内置心率应用程序。与心脏不断变大或变小不同,我希望让它跳动(两个方向的动画),然后在再次跳动之前暂停片刻(两个方向的动画),然后再次暂停,依此类推

例如,当我在
.repeatForever()
之前使用
.delay(1)
添加一秒延迟时,动画会在每个节拍的中间暂停。例如,它变小,暂停,然后变大,然后暂停,等等

我理解为什么会发生这种情况,但是如何在每个自动反转重复之间插入延迟,而不是在自动反转重复的两端插入延迟?

我相信我可以计算出延迟的时间长度和每个拍的长度,以使一切正常工作,因此延迟长度可以是任意的,但我想得到的是有关如何在动画循环之间实现暂停的帮助


我使用的一种方法是
flatMap
currentBPM
转换为每当我获得新的心率BPM时重复发布的
Timer
s,这样我就可以尝试从中驱动动画,但我不确定如何在SwiftUI中将其转化为动画,我也不确定当计时似乎应由动画处理时,以这种方式手动驱动值是否是正确的方法,根据我目前对SwiftUI的理解。

一个可能的解决方案是使用
DispatchQueue.main.asyncAfter
链接单个动画片段。这使您可以控制何时延迟特定部件

下面是一个演示:


在pawello2222回答之前,我还在尝试让它工作,我想出了一个解决方案,使用
Timer
publisher和联合收割机框架

我已经包含了下面的代码,但这不是一个好的解决方案,因为每次
currentBPM
更改时,都会在下一个动画开始之前添加一个新的延迟。pawello2222的答案更好,因为它总是允许当前跳动动画完成,然后以更新的速率开始下一个循环

另外,我认为我在这里的答案不是很好,因为很多动画工作是在数据存储对象中完成的,而不是封装在视图中,在视图中可能更有意义

import SwiftUI
import Combine

class DataStore: ObservableObject {
    
    @Published var shouldBeSmall: Bool = false
    
    @Published var currentBPM: Int = 0
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        
        let newLengthPublisher =
            $currentBPM
            .map { 60 / Double($0) }
            .share()
        
        newLengthPublisher
            .delay(for: .seconds(0.2),
                   scheduler: RunLoop.main)
            .map { beatLength in
                return Timer.publish(every: beatLength,
                                     on: .main,
                                     in: .common)
                    .autoconnect()
            }
            .switchToLatest()
            .sink { timer in
                self.shouldBeSmall = false
            }
            .store(in: &cancellables)
        
        newLengthPublisher
            .map { beatLength in
                return Timer.publish(every: beatLength,
                                     on: .main,
                                     in: .common)
                    .autoconnect()
            }
            .switchToLatest()
            .sink { timer in
                self.shouldBeSmall = true
            }
            .store(in: &cancellables)
        
        currentBPM = 75
    }
}

struct ContentView: View {
    
    @ObservedObject var store = DataStore()
    
    private let minScale: CGFloat = 0.8
    
    var body: some View {
        
        HStack {
            
            Image(systemName: "heart.fill")
                .font(.largeTitle)
                .foregroundColor(.red)
                .scaleEffect(store.shouldBeSmall ? 1 : minScale)
                .animation(.easeIn)
            
            Text("\(store.currentBPM)")
                .font(.largeTitle)
                .fontWeight(.bold)
        }
    }
}
导入快捷界面
进口联合收割机
类数据存储:observeObject{
@已发布的变量shouldBeSmall:Bool=false
@已发布变量currentBPM:Int=0
private var cancelables=Set()
init(){
让newLengthPublisher=
$currentBPM
.map{60/双($0)}
.share()
纽伦兹出版社
.延迟(持续:。秒(0.2),
调度程序:RunLoop.main)
.map{beatLength in
返回计时器。发布(每:beatLength,
主要,
中:。通用)
.自动连接()
}
.switchToLatest()
.sink{timer in
self.shouldBeSmall=false
}
.store(在:&可取消项中)
纽伦兹出版社
.map{beatLength in
返回计时器。发布(每:beatLength,
主要,
中:。通用)
.自动连接()
}
.switchToLatest()
.sink{timer in
self.shouldBeSmall=true
}
.store(在:&可取消项中)
当前BPM=75
}
}
结构ContentView:View{
@ObservedObject变量存储=数据存储()
私有let minScale:CGFloat=0.8
var body:一些观点{
HStack{
图像(系统名称:“heart.fill”)
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(store.shouldBeSmall?1:minScale)
.animation(.easeIn)
文本(“\(store.currentBPM)”)
.font(.largeTitle)
.fontWeight(.粗体)
}
}
}
private extension SimpleBeatingView {
    func startAnimation() {
        isBeating = true
        withAnimation(Animation.linear(duration: beatLength * 0.25)) {
            heartState = .large
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.25) {
            withAnimation(Animation.linear(duration: beatLength * 0.5)) {
                heartState = .small
            }
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.75) {
            withAnimation(Animation.linear(duration: beatLength * 0.25)) {
                heartState = .normal
            }
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + beatLength + beatDelay) {
            withAnimation {
                if isBeating {
                    startAnimation()
                }
            }
        }
    }

    func stopAnimation() {
        isBeating = false
    }
}
enum HeartState {
    case small, normal, large

    var scale: CGFloat {
        switch self {
        case .small: return 0.5
        case .normal: return 0.75
        case .large: return 1
        }
    }
}
import SwiftUI
import Combine

class DataStore: ObservableObject {
    
    @Published var shouldBeSmall: Bool = false
    
    @Published var currentBPM: Int = 0
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        
        let newLengthPublisher =
            $currentBPM
            .map { 60 / Double($0) }
            .share()
        
        newLengthPublisher
            .delay(for: .seconds(0.2),
                   scheduler: RunLoop.main)
            .map { beatLength in
                return Timer.publish(every: beatLength,
                                     on: .main,
                                     in: .common)
                    .autoconnect()
            }
            .switchToLatest()
            .sink { timer in
                self.shouldBeSmall = false
            }
            .store(in: &cancellables)
        
        newLengthPublisher
            .map { beatLength in
                return Timer.publish(every: beatLength,
                                     on: .main,
                                     in: .common)
                    .autoconnect()
            }
            .switchToLatest()
            .sink { timer in
                self.shouldBeSmall = true
            }
            .store(in: &cancellables)
        
        currentBPM = 75
    }
}

struct ContentView: View {
    
    @ObservedObject var store = DataStore()
    
    private let minScale: CGFloat = 0.8
    
    var body: some View {
        
        HStack {
            
            Image(systemName: "heart.fill")
                .font(.largeTitle)
                .foregroundColor(.red)
                .scaleEffect(store.shouldBeSmall ? 1 : minScale)
                .animation(.easeIn)
            
            Text("\(store.currentBPM)")
                .font(.largeTitle)
                .fontWeight(.bold)
        }
    }
}