延迟 SwiftUI 中的重复动画,在完整的自动反转重复周期之间
Posted
技术标签:
【中文标题】延迟 SwiftUI 中的重复动画,在完整的自动反转重复周期之间【英文标题】:Delay a repeating animation in SwiftUI with between full autoreverse repeat cycles 【发布时间】:2020-12-30 15:57:51 【问题描述】:我正在使用 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
我想让这个动画表现得更像 Apple 的内置心率应用程序。与其让心脏不断变大或变小,我想让它跳动(两个方向的动画)然后暂停片刻,然后再次跳动(动画两个方向)然后再次暂停,依此类推。
当我添加一秒延迟时,例如,.delay(1)
在.repeatForever()
之前,动画会在每个节拍中途暂停。例如,它变小,暂停,然后变大,然后暂停,等等。
我明白为什么会发生这种情况,但是如何在每个自动反转重复之间插入延迟,而不是在自动反转重复的两端插入?
我相信我可以计算出延迟应该多长以及每个节拍的长度以使一切正常进行,因此延迟长度可以是任意的,但我正在寻找的是帮助关于如何在动画循环之间实现暂停。
我使用的一种方法是在每次获得新的心率 BPM 时将flatMap
和currentBPM
重复发布的Timer
s,这样我就可以尝试从中驱动动画,但我不确定如何实际上,我可以将其转换为 SwiftUI 中的动画,但根据我目前对 SwiftUI 的理解,当时间似乎应该由动画处理时,我不确定以这种方式手动驱动值是否是正确的方法。
【问题讨论】:
【参考方案1】:一种可能的解决方案是使用DispatchQueue.main.asyncAfter
链接单个片段动画。这使您可以控制何时延迟特定部分。
这是一个演示:
struct SimpleBeatingView: View
@State private var isBeating = false
@State private var heartState: HeartState = .normal
@State private var beatLength: TimeInterval = 1
@State private var beatDelay: TimeInterval = 3
var body: some View
VStack
Image(systemName: "heart.fill")
.imageScale(.large)
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(heartState.scale)
Button("isBeating: \(String(isBeating))")
isBeating.toggle()
HStack
Text("beatLength")
Slider(value: $beatLength, in: 0.25...2)
HStack
Text("beatDelay")
Slider(value: $beatDelay, in: 0...5)
.onChange(of: isBeating) isBeating in
if isBeating
startAnimation()
else
stopAnimation()
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
【讨论】:
【参考方案2】:在 pawello2222 回答之前,我仍在尝试尝试使其正常工作,我想出了一个使用 Timer
发布者和组合框架的解决方案。
我已经包含了下面的代码,但这不是一个好的解决方案,因为每次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)
【讨论】:
以上是关于延迟 SwiftUI 中的重复动画,在完整的自动反转重复周期之间的主要内容,如果未能解决你的问题,请参考以下文章