Timer 用于倒计时的循环进度条publisher SwiftUI

Timer 用于倒计时的循环进度条publisher SwiftUI,timer,swiftui,progress-bar,combine,Timer,Swiftui,Progress Bar,Combine,下面是我的倒计时和循环进度条代码 我编写了一个函数makeProgressIncrement(),用于确定 计时器每秒的进度总计timeSelected 更新ProgressBar使其随倒计时时间增加的最佳方法是什么 我应该使用onReceive方法吗 非常感谢您的帮助 ContentView import SwiftUI import Combine struct ContentView: View { @StateObject var timer = TimerManager() @St

下面是我的倒计时和循环进度条代码

我编写了一个函数
makeProgressIncrement()
,用于确定 计时器每秒的进度总计
timeSelected

更新
ProgressBar
使其随倒计时时间增加的最佳方法是什么

我应该使用
onReceive
方法吗

非常感谢您的帮助

ContentView

import SwiftUI
import Combine

struct ContentView: View {

@StateObject var timer = TimerManager()
@State var progressValue : CGFloat = 0

var body: some View {
    
    ZStack{
        VStack {
            ZStack{
                
                ProgressBar(progress: self.$progressValue)
                    .frame(width: 300.0, height: 300)
                    .padding(40.0)
              
                VStack{
                    
                    Image(systemName: timer.isRunning ? "pause.fill" : "play.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 80, height: 80)
                        .foregroundColor(.blue)
                        .onTapGesture{
                            timer.isRunning ? timer.pause() : timer.start()
                        }
                }
            }

            Text(timer.timerString)
                .onAppear {
                    if timer.isRunning {
                        timer.stop()
                    }
                }
                .padding(.bottom, 100)
            
            
            Image(systemName: "stop.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 35, height: 35)
                .foregroundColor(.blue)
                .onTapGesture{
                    timer.stop()


                }
            }
        }
    }
 }
ProgressBar

struct ProgressBar: View {
    @Binding var progress: CGFloat
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 20.0)
                .opacity(0.3)
                .foregroundColor(Color.blue)
            
            Circle()
                .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
                .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.blue)
                .rotationEffect(Angle(degrees: 270.0))
                .animation(.linear)
            
        }
    }
}
class TimerManager: ObservableObject {
    
    /// Is the timer running?
    @Published private(set) var isRunning = false
    
    /// String to show in UI
    @Published private(set) var timerString = ""
    
    /// Timer subscription to receive publisher
    private var timer: AnyCancellable?
    
    /// Time that we're counting from & store it when app is in background
    private var startTime: Date? { didSet { saveStartTime() } }
    
    var timeSelected: Double = 30
    var timeRemaining: Double = 0
    var timePaused: Date = Date()
    var progressIncrement: Double = 0
    
    init() {
        startTime = fetchStartTime()
        
        if startTime != nil {
            start()
        }
    }
}

// MARK: - Public Interface

extension TimerManager {

    func start() {
     
        timer?.cancel()               
        
        if startTime == nil {
            startTime = Date()
        }
        
        timerString = ""
        
        timer = Timer
            .publish(every: 0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard
                    let self = self,
                    let startTime = self.startTime
                else { return }
                
                let now = Date()
                let elapsedTime = now.timeIntervalSince(startTime)
                      
                self.timeRemaining = self.timeSelected - elapsedTime
                                    
                guard self.timeRemaining > 0 else {
                    self.stop()
                    return
                }
                self.timerString = String(format: "%0.1f", self.timeRemaining)
            }
        isRunning = true
    }
    
    func stop() {
        timer?.cancel()
        timeSelected = 300
        timer = nil
        startTime = nil
        isRunning = false
        timerString = " "
    }
    
    func pause() {
        timeSelected = timeRemaining
        timer?.cancel()
        startTime = nil
        timer = nil
        isRunning = false
    }
    
    func makeProgressIncrement() -> CGFloat{
        
        progressIncrement = 1 / timeSelected
        
        return CGFloat(progressIncrement)
        
    }        
}

private extension TimerManager {

    func saveStartTime() {
        if let startTime = startTime {
            UserDefaults.standard.set(startTime, forKey: "startTime")
        } else {
            UserDefaults.standard.removeObject(forKey: "startTime")
        }
    }
    
    func fetchStartTime() -> Date? {
        UserDefaults.standard.object(forKey: "startTime") as? Date
    }
}
TimerManager

struct ProgressBar: View {
    @Binding var progress: CGFloat
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(lineWidth: 20.0)
                .opacity(0.3)
                .foregroundColor(Color.blue)
            
            Circle()
                .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
                .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                .foregroundColor(Color.blue)
                .rotationEffect(Angle(degrees: 270.0))
                .animation(.linear)
            
        }
    }
}
class TimerManager: ObservableObject {
    
    /// Is the timer running?
    @Published private(set) var isRunning = false
    
    /// String to show in UI
    @Published private(set) var timerString = ""
    
    /// Timer subscription to receive publisher
    private var timer: AnyCancellable?
    
    /// Time that we're counting from & store it when app is in background
    private var startTime: Date? { didSet { saveStartTime() } }
    
    var timeSelected: Double = 30
    var timeRemaining: Double = 0
    var timePaused: Date = Date()
    var progressIncrement: Double = 0
    
    init() {
        startTime = fetchStartTime()
        
        if startTime != nil {
            start()
        }
    }
}

// MARK: - Public Interface

extension TimerManager {

    func start() {
     
        timer?.cancel()               
        
        if startTime == nil {
            startTime = Date()
        }
        
        timerString = ""
        
        timer = Timer
            .publish(every: 0, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard
                    let self = self,
                    let startTime = self.startTime
                else { return }
                
                let now = Date()
                let elapsedTime = now.timeIntervalSince(startTime)
                      
                self.timeRemaining = self.timeSelected - elapsedTime
                                    
                guard self.timeRemaining > 0 else {
                    self.stop()
                    return
                }
                self.timerString = String(format: "%0.1f", self.timeRemaining)
            }
        isRunning = true
    }
    
    func stop() {
        timer?.cancel()
        timeSelected = 300
        timer = nil
        startTime = nil
        isRunning = false
        timerString = " "
    }
    
    func pause() {
        timeSelected = timeRemaining
        timer?.cancel()
        startTime = nil
        timer = nil
        isRunning = false
    }
    
    func makeProgressIncrement() -> CGFloat{
        
        progressIncrement = 1 / timeSelected
        
        return CGFloat(progressIncrement)
        
    }        
}

private extension TimerManager {

    func saveStartTime() {
        if let startTime = startTime {
            UserDefaults.standard.set(startTime, forKey: "startTime")
        } else {
            UserDefaults.standard.removeObject(forKey: "startTime")
        }
    }
    
    func fetchStartTime() -> Date? {
        UserDefaults.standard.object(forKey: "startTime") as? Date
    }
}

您可以在
TimeManager
中创建计算进度的computed属性:

extension TimerManager {
  var progress: CGFloat {
     return CGFloat(timeRemaining / timeSelected)
  }
}
但你也需要一个触发器,让观察者告诉他们它已经改变了

由于该值取决于
timeRemaining
属性,该属性是
@Published
,因此它将起作用,因为观察对象将注意到一个更改并再次请求计算值(该值也会更改)

或者,您可以在
.sink
中调用
self.objectWillChange.send()
,通知对象将发生更改,这将完成相同的任务

一旦有了它,您可以在视图中直接引用它:

ProgressBar(progress: self.timer.progress)

(并更改
ProgressBar
,使其
.progress
属性不是绑定。

您可以使用
.onReceive
。或者,您可以将其作为
timermager
类的计算属性并直接使用,例如:
self.$timer.progressIncrement
@NewDev>.onReceive但我收到一个错误,该错误与“Publisher”不符。由于类中有
progressIncrement
,我如何直接使用它来更新ProgressBar
progressValue
?感谢您的帮助。我使用计算属性得到以下错误:无法分配到属性:“progress”是仅获取属性。我尝试了获取/设置,但进度条仍不能在计时器倒计时的每一秒正确更新。您到底在分配什么?其思想是此属性是根据
timeSelected
计算的,因此您不应该为其分配任何内容-只需设置
timeSelected
进度条即可(progress:self.$timer.progress)
在尝试传递计时器时抛出错误。我明白了。
ProgressBar
接受它作为绑定。但是为什么?除非我缺少什么,否则它应该只是一个常规参数。(我更改了答案)如果我将
ProgressBar
结构中的变量更改为非绑定,则错误将消失,但进度条仍与计时器不同步。它仅在启动
timer.start()
时触发一次。