Reputation: 127
Below is my code for a countdown timer and circular progress bar.
I coded a function makeProgressIncrement()
that determines
the progress per second from the timer total of timeSelected
.
What is the best way to to update the ProgressBar
so it increases with the countdown timer publisher?
Should I use an onReceive
method?
Any help is greatly appreciated.
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)
}
}
}
TimerManager
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
}
}
Upvotes: 1
Views: 1830
Reputation: 49590
You can create a computed property in TimeManager
that calculates the progress:
extension TimerManager {
var progress: CGFloat {
return CGFloat(timeRemaining / timeSelected)
}
}
But you also need a trigger for the observers to tell them that it's changed.
Since this value depends on a timeRemaining
property, which is @Published
, it would work because the observing object will notice a change and ask for the computed value again (which would also change).
Alternatively, you can call self.objectWillChange.send()
inside the .sink
to notify that an object will change, and that will accomplish the same thing.
Once you have that, you can just refer to it directly in your views:
ProgressBar(progress: self.timer.progress)
(and change ProgressBar
so that its .progress
property isn't a binding.
Upvotes: 1