// // 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) } }