SwiftUI中在自动反转重复动画的循环间添加延迟
解决SwiftUI中Watch心率动画同步循环+暂停的问题
你遇到的问题很典型——SwiftUI的repeatForever(autoreverses: true)会把延迟加在动画的“去程”和“回程”两端,而不是整个双向动画完成后的暂停。下面给你两种实用的方案,完美实现原生心率App的“跳动→还原→暂停→重复”效果,还能适配心率变化:
方案一:用Animation.sequence组合动画序列
这种方式利用SwiftUI的动画序列功能,把“双向跳动动画”和“空延迟动画”组合成一个完整循环,再永久重复。代码简洁,适合需求相对固定的场景:
struct SimpleBeatingView: View { @Published var currentBPM: Int = 80 // 改为@Published,支持动态更新 @State private var isBeating = false private let maxScale: CGFloat = 0.8 private let beatAnimationDuration: Double = 0.2 // 单次双向跳动的动画时长,可按需调整 var fullCycleAnimation: Animation { let totalCycleLength = 60 / Double(currentBPM) // 两次跳动的间隔总时长 let pauseLength = totalCycleLength - beatAnimationDuration return Animation.sequence([ // 双向跳动:从缩小状态到正常,再回到缩小(autoreverse自动反转) Animation.easeInOut(duration: beatAnimationDuration) .autoreverse(), // 暂停阶段:播放一个无状态变化的线性动画,占满剩余时间 Animation.linear(duration: pauseLength) ]) .repeatForever(autoreverses: false) // 整个序列永久重复,不反转 } var body: some View { Image(systemName: "heart.fill") .font(.largeTitle) .foregroundColor(.red) .scaleEffect(isBeating ? 1 : maxScale) .animation(fullCycleAnimation, value: isBeating) // 绑定状态值触发动画 .onAppear { self.isBeating = true } .onChange(of: currentBPM) { _ in // 心率变化时,重置动画状态以加载新的循环参数 self.isBeating = false DispatchQueue.main.async { self.isBeating = true } } } }
方案解析:
- 把整个循环拆分为双向跳动动画和暂停占位动画两部分,用
Animation.sequence串联 repeatForever(autoreverses: false)确保整个序列(跳动+暂停)重复,而不是单个动画反转- 当心率变化时,通过重置
isBeating状态来触发新参数的动画
方案二:手动递归控制动画循环(更灵活)
如果需要更精细地控制每个阶段的时间或状态,比如调整跳动的快慢、添加额外视觉效果,手动递归调用withAnimation是更好的选择:
struct SimpleBeatingView: View { @Published var currentBPM: Int = 80 @State private var isScaledUp = false private let maxScale: CGFloat = 0.8 private let beatAnimationDuration: Double = 0.2 // 单次缩放的时长 private var totalCycleLength: Double { 60 / Double(currentBPM) } private var pauseLength: Double { totalCycleLength - (beatAnimationDuration * 2) } // 双向跳动后的暂停时长 var body: some View { Image(systemName: "heart.fill") .font(.largeTitle) .foregroundColor(.red) .scaleEffect(isScaledUp ? 1 : maxScale) .onAppear { startBeatingCycle() } .onChange(of: currentBPM) { _ in startBeatingCycle() // 心率变化时重启循环 } } private func startBeatingCycle() { // 重置状态,确保新循环从初始缩小状态开始 isScaledUp = false beatOnce() } private func beatOnce() { // 第一步:心脏放大(跳动的“鼓起来”阶段) withAnimation(Animation.easeInOut(duration: beatAnimationDuration)) { isScaledUp = true } // 第二步:心脏缩小回到初始状态(完成双向跳动) DispatchQueue.main.asyncAfter(deadline: .now() + beatAnimationDuration) { withAnimation(Animation.easeInOut(duration: beatAnimationDuration)) { isScaledUp = false } // 第三步:暂停指定时长后,开启下一次跳动循环 DispatchQueue.main.asyncAfter(deadline: .now() + pauseLength) { beatOnce() } } } }
方案解析:
- 通过递归调用
beatOnce()实现无限循环,每个循环包含放大→缩小→暂停三个阶段 - 每个阶段的时长都可以单独调整,灵活性极高
- 心率变化时,调用
startBeatingCycle()重置状态并启动新的循环,完美适配新的心率间隔
这两种方案都能实现你想要的效果,你可以根据自己的需求选择。如果只是基础的跳动+暂停,方案一足够简洁;如果需要扩展更多动画细节,方案二更适合。
内容的提问来源于stack exchange,提问作者gohnjanotis




