Thom Sides
Thom Sides

Reputation: 127

Circular progress bar for a countdown timer publisher SwiftUI

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

Answers (1)

New Dev
New Dev

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

Related Questions