You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动