Categories
coding

A stopwatch timer class for SwiftUI

I spent sometime recently for Beep Test Watch creating a timer class in swift that publishes a nicely formatted string with the timer as “00:00” being mm:ss. Figured I would share it here if anyone stumbled upon it and found it useful.

import Combine
import Foundation

class TimerManager: NSObject, ObservableObject {
    @Published var elapsedSeconds: Int16 = 0
    @Published var elapsedSecondsFormatted: String = "00:00"

    var start: Date = Date()
    // Holds timer
    var cancellable: Cancellable?
    var accumulatedTime: Int16 = 0

    func setUpTimer() {
        start = Date()
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.elapsedSeconds = self.incrementElapsedTime()
                self.elapsedSecondsFormatted = self.getFormatedTime(seconds: self.elapsedSeconds)
            }
    }

    // Calculate the elapsed time.
    func incrementElapsedTime() -> Int16 {
        let runningTime: Int16 = Int16(-1 * (self.start.timeIntervalSinceNow))
        return self.accumulatedTime + runningTime
    }

    func endTimer() {
        // Cancel timer and set elapsedSeconds Publisher to 0
        cancellable?.cancel()
        elapsedSeconds = 0
    }

    // Creates spotwatch formatted version of time
    func getFormatedTime(seconds: Int16) -> String {
        return self.elapsedTimeString(elapsed: self.secondsToHoursMinutesSeconds(seconds: seconds))
    }

    // Convert the seconds into seconds, minutes, hours.
    func secondsToHoursMinutesSeconds (seconds: Int16) -> (Int16, Int16, Int16) {
        return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
    }

    // Convert the seconds, minutes, hours into a string.
    func elapsedTimeString(elapsed: (h: Int16, m: Int16, s: Int16)) -> String {
        return String(format: "%02d:%02d", elapsed.m, elapsed.s)
    }
}

In order to use it add an environmental object to your ContentView() in your main app file.

import SwiftUI
var timerManager = TimerManager()

@main
struct Your_App: App {
    var body: some Scene {
        WindowGroup {
                ContentView()
                    .environmentObject(timerManager)
        }
    }
}


struct ContentView: View {
    @EnvironmentObject var timerManager: TimerManage
    var body: some View { ... }
}

I’ve made a playground file as well that works standalone that shows you how to use it below.

import Combine
import Foundation
import SwiftUI
import PlaygroundSupport


// A timer class that publishes elapsedSeconds since setUpTimer was called as in integer as well as elapsedSecondsFormatted a formatted version that is "mm:ss", note the hour is dropped. Must call endTimer when timer is no longer needed to cancel timer.
class TimerManager: NSObject, ObservableObject {
    @Published var elapsedSeconds: Int16 = 0
    @Published var elapsedSecondsFormatted: String = "00:00"

    var start: Date = Date()
    // Holds timer
    var cancellable: Cancellable?
    var accumulatedTime: Int16 = 0

    func setUpTimer() {
        start = Date()
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.elapsedSeconds = self.incrementElapsedTime()
                self.elapsedSecondsFormatted = self.getFormatedTime(seconds: self.elapsedSeconds)
            }
    }

    // Calculate the elapsed time.
    func incrementElapsedTime() -> Int16 {
        let runningTime: Int16 = Int16(-1 * (self.start.timeIntervalSinceNow))
        return self.accumulatedTime + runningTime
    }

    func endTimer() {
        // Cancel timer and set elapsedSeconds Publisher to 0
        cancellable?.cancel()
        elapsedSeconds = 0
    }

    // Creates spotwatch formatted version of time
    func getFormatedTime(seconds: Int16) -> String {
        return self.elapsedTimeString(elapsed: self.secondsToHoursMinutesSeconds(seconds: seconds))
    }

    // Convert the seconds into seconds, minutes, hours.
    func secondsToHoursMinutesSeconds (seconds: Int16) -> (Int16, Int16, Int16) {
        return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
    }

    // Convert the seconds, minutes, hours into a string.
    func elapsedTimeString(elapsed: (h: Int16, m: Int16, s: Int16)) -> String {
        return String(format: "%02d:%02d", elapsed.m, elapsed.s)
    }
}

// Maybe publish the formatted version as well to remove redundant code.

var timerManager = TimerManager()

struct ContentView: View {

    @EnvironmentObject var timerManager: TimerManager
    var body: some View {
        Text("TIME")
            .font(.system(size: 12))
            .foregroundColor(.red)
        Text("\(timerManager.elapsedSecondsFormatted)")
            .padding(.top, -8.0)
            .font(Font.system(size: 16, weight: .semibold, design: .default).monospacedDigit())
        Button("start", action: startTimer)
        Button("end", action: endTimer)
    }
    func startTimer() {
        timerManager.setUpTimer()
    }
    func endTimer() {
        timerManager.endTimer()
    }

}

PlaygroundPage.current.setLiveView(
    ContentView()
        .environmentObject(timerManager)
)