Files
multi-chrono-ios/MultiChrono/Stopwatch.swift
2026-01-27 02:39:24 +01:00

148 lines
4.8 KiB
Swift

//
// Stopwatch.swift
// MultiChrono
//
// Created by Beatrice Dellacà on 26/01/26.
//
import Foundation
import Combine
class Stopwatch: ObservableObject, Identifiable, Codable {
let id: UUID
@Published var name: String
@Published var elapsedTime: TimeInterval = 0
@Published var isRunning: Bool = false
private var timer: AnyCancellable?
private var startTime: Date?
private var accumulatedTime: TimeInterval = 0
enum CodingKeys: String, CodingKey {
case id, name, elapsedTime, isRunning, startTime, accumulatedTime
}
init(name: String) {
self.id = UUID()
self.name = name
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
elapsedTime = try container.decode(TimeInterval.self, forKey: .elapsedTime)
isRunning = try container.decode(Bool.self, forKey: .isRunning)
startTime = try container.decodeIfPresent(Date.self, forKey: .startTime)
accumulatedTime = try container.decode(TimeInterval.self, forKey: .accumulatedTime)
if isRunning {
// Restart the timer if it was running.
// We also need to update current elapsedTime based on how much time passed since startTime
if let start = startTime {
elapsedTime = accumulatedTime + Date().timeIntervalSince(start)
}
startTimer()
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(elapsedTime, forKey: .elapsedTime)
try container.encode(isRunning, forKey: .isRunning)
try container.encode(startTime, forKey: .startTime)
try container.encode(accumulatedTime, forKey: .accumulatedTime)
}
deinit {
timer?.cancel()
}
func start() {
guard !isRunning else { return }
startTime = Date()
isRunning = true
startTimer()
}
private func startTimer() {
timer = Timer.publish(every: 0.001, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.tick()
}
}
func pause() {
guard isRunning else { return }
timer?.cancel()
timer = nil
if let startTime = startTime {
accumulatedTime += Date().timeIntervalSince(startTime)
}
isRunning = false
startTime = nil
// Ensure UI shows accurate accumulated time
elapsedTime = accumulatedTime
}
func reset() {
pause()
accumulatedTime = 0
elapsedTime = 0
}
private func tick() {
guard let startTime = startTime else { return }
elapsedTime = accumulatedTime + Date().timeIntervalSince(startTime)
}
func formattedTime(format: TimeFormat = .cents) -> String {
let hours = Int(elapsedTime) / 3600
let minutes = Int(elapsedTime) / 60 % 60
let seconds = Int(elapsedTime) % 60
switch format {
case .millis:
let millis = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 1000)
if hours > 0 {
return String(format: "%02i:%02i:%02i.%03i", hours, minutes, seconds, millis)
} else {
return String(format: "%02i:%02i.%03i", minutes, seconds, millis)
}
case .cents:
let cents = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 100)
if hours > 0 {
return String(format: "%02i:%02i:%02i.%02i", hours, minutes, seconds, cents)
} else {
return String(format: "%02i:%02i.%02i", minutes, seconds, cents)
}
case .tenths:
let tenths = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 10)
if hours > 0 {
return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths)
} else {
return String(format: "%02i:%02i.%01i", minutes, seconds, tenths)
}
case .seconds:
if hours > 0 {
return String(format: "%02i:%02i:%02i", hours, minutes, seconds)
} else {
return String(format: "%02i:%02i", minutes, seconds)
}
}
}
// Legacy support to avoid breaking existing code immediately
var formattedTime: String {
formattedTime(format: AppSettings.shared.timeFormat)
}
}