如何在SwiftUI中获取多个Timer来对Shape进行适当的动画处理? [英] How can I get multiple Timers to appropriately animate a Shape in SwiftUI?

查看:59
本文介绍了如何在SwiftUI中获取多个Timer来对Shape进行适当的动画处理?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为任何俗气的代码预先致歉(我仍在学习Swift和SwiftUI).

Apologies in advance for any tacky code (I'm still learning Swift and SwiftUI).

我有一个项目,我想让一个数组中的多个计时器递减计数到零,一次计数一次.当用户单击开始"时,数组中的第一个计时器递减计数并完成操作,然后调用完成处理程序,该处理程序使用 removeFirst()将数组向左移动,并启动下一个计时器(现在是第一个计时器).列表中的计时器),直到所有计时器都完成.

I have a project where I'd like to have multiple timers in an Array count down to zero, one at a time. When the user clicks start, the first timer in the Array counts down and finishes, and a completion handler is called which shifts the array to the left with removeFirst() and starts the next timer (now the first timer in the list) and does this until all timers are done.

我还有一个名为DissolvingCircle的自定义形状,类似于Apple的本机iOS倒数计时器,它会在计时器倒数时擦除自身,并在用户单击暂停时停止溶解.

I also have a custom Shape called DissolvingCircle, which like Apple's native iOS countdown timer, erases itself as the timer counts down, and stops dissolving when the user clicks pause.

我遇到的问题是该动画仅适用于列表中的第一个计时器.溶解后,第二个计时器启动时它不会回来.好吧,至少不完全正确:如果在第二个计时器运行时单击暂停",则会绘制适当的形状.然后,当我再次单击开始"时,第二个计时器动画会适当显示,直到该计时器结束.

The problem I'm running into is that the animation only works for the first timer in the list. After it dissolves, it does not come back when the second timer starts. Well, not exactly at least: if I click pause while the second timer is running, the appropriate shape is drawn. Then when I click start again, the second timer animation shows up appropriately until that timer ends.

我怀疑问题与我正在进行的状态检查有关.仅当 timer.status == .running 时,动画才会开始.在第一个计时器结束的那一刻,其状态设置为 .stopped ,然后在换档期间下降,然后新的计时器启动并设置为 .running ,因此即使正在运行新的计时器,我的ContentView似乎也看不到任何状态变化.我尝试研究SwiftUI中Shape动画的一些基本原理,并尝试重新考虑如何设置计时器的状态以获得所需的行为,但是我无法提出可行的解决方案.

I'm suspecting the issue has to do with the state check I'm making. The animation only starts if timer.status == .running. In that moment when the first timer ends, its status gets set to .stopped, then it falls off during the shift, and then the new timer starts and is set to .running, so my ContentView doesn't appear to see any change in state, even though there is a new timer running. I tried researching some basic principles of Shape animations in SwiftUI and tried re-thinking how my timer's status is being set to get the desired behavior, but I can't come up with a working solution.

如何最好地为列表中的下一个计时器重新启动动画?

How can I best restart the animation for the next timer in my list?

这是我下面有问题的代码:

Here is my problematic code below:

MyTimer类-每个单独的计时器.我在此处设置计时器状态,并调用计时器完成时作为闭包传递的完成处理程序.

MyTimer Class - each individual timer. I set the timer status here, as well as call the completion handler passed as a closure when the timer is finished.

//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的类进行管理,

The list of timers is managed by a class I created called MyTimerManager, here:

//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")
    }    
}

最后,ContentView:

Finally, the ContentView:

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. 第二个计时器正在运行,动画现在消失了.

  1. 我在第三个计时器上单击了暂停,ContentView注意到状态发生了变化.如果我单击从此处开始",则动画将再次起作用,直到计时器结束.

请告知我是否可以提供其他代码或讨论.我也很高兴收到一些建议,以使代码的其他部分更加美观.

Please let me know if I can provide any additional code or discussion. I'm glad to also receive recommendations to make other parts of my code more elegant.

在此先感谢您的任何建议或帮助!

Thank you in advance for any suggestions or assistance!

推荐答案

我可能找到了一个合适的答案,类似于

I may have found one appropriate answer, similar to one of the answers in How can I get data from ObservedObject with onReceive in SwiftUI? describing the use of .onReceive(_:perform:) with an ObservableObject.

而不是在ContentView的条件下显示计时器的状态,例如 if timer?.status == .running ,然后在 .onAppear 期间执行计时器动画功能,而是将计时器的状态传递给 .onReceive 像这样:

Instead of presenting the timer's status in a conditional to the ContentView, e.g. if timer?.status == .running and then executing the timer animation function during .onAppear, instead I passed the timer's status to .onReceive like this:

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()
    }
}

视图将接收新计时器的状态,并在新计时器启动时播放动画.

The view receives the new timer's status, and plays the animation when the new timer starts.

这篇关于如何在SwiftUI中获取多个Timer来对Shape进行适当的动画处理?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆