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