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)
)
One reply on “A stopwatch timer class for SwiftUI”
Fantastic post, extremely useful and refreshing to see someone share their ready-to-go solution for a common issue.