簡體   English   中英

如何讓多個計時器適當地為 SwiftUI 中的形狀設置動畫?

[英]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()
                }
            }
        }
    }
}

截圖:

  1. 第一個計時器運行,動畫正確。
  1. 第二個計時器運行,animation 現在不見了。
  1. 我在第三個計時器上單擊了暫停,ContentView 注意到 state 發生了變化。 如果我從這里單擊開始,animation 將再次工作,直到計時器結束。

如果我可以提供任何其他代碼或討論,請告訴我。 我也很高興收到讓我的代碼的其他部分更優雅的建議。

提前感謝您的任何建議或幫助!

我可能找到了一個合適的答案,類似於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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM