Below is my code for a countdown timer and circular progress bar.
I created a variable that determines the progress per second progressIncrement from the timer total of timeSelected.
What is the best way to to update the ProgressBar so it increases and is synchronized with the countdown timer publisher?
Any help is greatly appreciated.
ContentView
ProgessBar
TimerManager
I created a variable that determines the progress per second progressIncrement from the timer total of timeSelected.
What is the best way to to update the ProgressBar so it increases and is synchronized with the countdown timer publisher?
Any help is greatly appreciated.
ContentView
Code Block import Combine import SwiftUI 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() } } } } }
ProgessBar
Code Block 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) } } }
TimerManager
Code Block 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: CGFloat { return CGFloat(1/timeSelected) } 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 } } 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 } }