[英]How can I get multiple Timers to appropriately animate a Shape in SwiftUI?
提前為任何俗氣的代碼道歉(我仍在學習 Swift 和 SwiftUI)。
我有一個項目,我想在一個數組中有多個計時器倒計時到零,一次一個。 當用戶單擊開始時,數組中的第一個計時器倒計時並完成,並調用完成處理程序,該處理程序使用removeFirst()
將數組向左移動並啟動下一個計時器(現在是列表中的第一個計時器)並執行這直到所有計時器完成。
我還有一個名為 DissolvingCircle 的自定義形狀,它類似於 Apple 的原生 iOS 倒計時計時器,在計時器倒計時時自行擦除,並在用戶單擊暫停時停止溶解。
我遇到的問題是 animation 僅適用於列表中的第一個計時器。 溶解后,第二個計時器開始時它不會回來。 好吧,至少不完全是:如果我在第二個計時器運行時單擊暫停,則會繪制適當的形狀。 然后,當我再次單擊開始時,第二個計時器 animation 會正確顯示,直到該計時器結束。
我懷疑這個問題與我正在做的 state 檢查有關。 animation 僅在timer.status ==.running
時啟動。 在第一個計時器結束的那一刻,它的狀態設置為.stopped
,然后在輪班期間下降,然后新計時器啟動並設置為.running
,所以我的 ContentView 似乎沒有看到任何變化state,即使有一個新的計時器正在運行。 我嘗試在 SwiftUI 中研究形狀動畫的一些基本原理,並嘗試重新考慮如何設置計時器的狀態以獲得所需的行為,但我無法提出一個可行的解決方案。
我怎樣才能最好地重新啟動 animation 以用於我列表中的下一個計時器?
下面是我有問題的代碼:
MyTimer Class - 每個單獨的計時器。 我在這里設置了計時器狀態,並在計時器完成時調用作為閉包傳遞的完成處理程序。
//MyTimer.swift
import SwiftUI
class MyTimer: ObservableObject {
var timeLimit: Int
var timeRemaining: Int
var timer = Timer()
var onTick: (Int) -> ()
var completionHandler: () -> ()
enum TimerStatus {
case stopped
case paused
case running
}
@Published var status: TimerStatus = .stopped
init(duration timeLimit: Int, onTick: @escaping (Int) -> (), completionHandler: @escaping () -> () ) {
self.timeLimit = timeLimit
self.timeRemaining = timeLimit
self.onTick = onTick //will call each time the timer fires
self.completionHandler = completionHandler
}
func start() {
status = .running
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if (self.timeRemaining > 0) {
self.timeRemaining -= 1
print("Timer status: \(self.status) : \(self.timeRemaining)" )
self.onTick(self.timeRemaining)
if (self.timeRemaining == 0) { //time's up!
self.stop()
}
}
}
}
func stop() {
timer.invalidate()
status = .stopped
completionHandler()
print("Timer status: \(self.status)")
}
func pause() {
timer.invalidate()
status = .paused
print("Timer status: \(self.status)")
}
}
計時器列表由我創建的名為 MyTimerManager 的 class 管理,此處:
//MyTimerManager.swift
import SwiftUI
class MyTimerManager: ObservableObject {
var timerList = [MyTimer]()
@Published var timeRemaining: Int = 0
var timeLimit: Int = 0
init() {
//for testing purposes, let's create 3 timers, each with different durations.
timerList.append(MyTimer(duration: 10, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
timerList.append(MyTimer(duration: 7, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
timerList.append(MyTimer(duration: 11, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
self.timeLimit = timerList[0].timeLimit
self.timeRemaining = timerList[0].timeRemaining
}
func updateTime(_ timeRemaining: Int) {
self.timeRemaining = timeRemaining
}
//the completion handler - where the timer gets shifted off and the new timer starts
func myTimerDidFinish() {
timerList.removeFirst()
if timerList.isEmpty {
print("All timers finished")
} else {
self.timeLimit = timerList[0].timeLimit
self.timeRemaining = timerList[0].timeRemaining
timerList[0].start()
}
print("myTimerDidFinish() complete")
}
}
最后,內容視圖:
import SwiftUI
struct ContentView: View {
@ObservedObject var timerManager: MyTimerManager
//The following var and function take the time remaining, express it as a fraction for the first
//state of the animation, and the second state of the animation will be set to zero.
@State private var animatedTimeRemaining: Double = 0
private func startTimerAnimation() {
let timer = timerManager.timerList.isEmpty ? nil : timerManager.timerList[0]
animatedTimeRemaining = Double(timer!.timeRemaining) / Double(timer!.timeLimit)
withAnimation(.linear(duration: Double(timer!.timeRemaining))) {
animatedTimeRemaining = 0
}
}
var body: some View {
VStack {
let timer = timerManager.timerList.isEmpty ? nil : timerManager.timerList[0]
let displayText = String(timerManager.timeRemaining)
ZStack {
Text(displayText)
.font(.largeTitle)
.foregroundColor(.black)
//This is where the problem is occurring. When the first timer starts, it gets set to
//.running, and so the animation runs approp, however, after the first timer ends, and
//the second timer begins, there appears to be no state change detected and nothing happens.
if timer?.status == .running {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(animatedTimeRemaining*360-90))
.onAppear {
self.startTimerAnimation()
}
//this code is mostly working approp when I click pause.
} else if timer?.status == .paused || timer?.status == .stopped {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(Double(timer!.timeRemaining) / Double(timer!.timeLimit)*360-90))
}
}
HStack {
Button(action: {
print("Cancel button clicked")
timerManager.objectWillChange.send()
timerManager.stop()
}) {
Text("Cancel")
}
switch (timer?.status) {
case .stopped, .paused:
Button(action: {
print("Start button clicked")
timerManager.objectWillChange.send()
timer?.start()
}) {
Text("Start")
}
case .running:
Button(action: {
print("Pause button clicked")
timerManager.objectWillChange.send()
timer?.pause()
}){
Text("Pause")
}
case .none:
EmptyView()
}
}
}
}
}
截圖:
如果我可以提供任何其他代碼或討論,請告訴我。 我也很高興收到讓我的代碼的其他部分更優雅的建議。
提前感謝您的任何建議或幫助!
我可能找到了一個合適的答案,類似於How can I get data from ObservedObject with onReceive in SwiftUI? 描述.onReceive(_:perform:)
與 ObservableObject 的使用。
不是在 ContentView 的條件下顯示計時器的狀態,例如if timer?.status ==.running
然后在 .onAppear 期間執行計時器 animation .onAppear
,而是像這樣將計時器的狀態傳遞給.onReceive
:
if timer?.status == .paused || timer?.status == .stopped {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(Double(timer!.timeRemaining) / Double(timer!.timeLimit)*360-90))
} else {
if let t = timer {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(animatedTimeRemaining*360-90))
.onReceive(t.$status) { _ in
self.startTimerAnimation()
}
}
視圖接收新定時器的狀態,並在新定時器啟動時播放 animation。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.