diff --git a/MultiChrono/ContentView.swift b/MultiChrono/ContentView.swift index 0a23971..cff5da1 100644 --- a/MultiChrono/ContentView.swift +++ b/MultiChrono/ContentView.swift @@ -11,14 +11,22 @@ import Combine class ContentViewModel: ObservableObject { @Published var stopwatches: [Stopwatch] = [] + private let saveKey = "SavedStopwatches" + + init() { + load() + } + func addStopwatch(name: String) { let newStopwatch = Stopwatch(name: name) stopwatches.append(newStopwatch) + save() } func deleteStopwatch(at index: Int) { stopwatches[index].pause() // Ensure timer is stopped stopwatches.remove(at: index) + save() } func deleteStopwatch(id: UUID) { @@ -26,11 +34,26 @@ class ContentViewModel: ObservableObject { deleteStopwatch(at: index) } } + + func save() { + if let encoded = try? JSONEncoder().encode(stopwatches) { + UserDefaults.standard.set(encoded, forKey: saveKey) + } + } + + func load() { + if let data = UserDefaults.standard.data(forKey: saveKey) { + if let decoded = try? JSONDecoder().decode([Stopwatch].self, from: data) { + stopwatches = decoded + } + } + } } struct ContentView: View { @StateObject private var viewModel = ContentViewModel() @State private var isShowingAddSheet = false + @Environment(\.scenePhase) private var scenePhase var body: some View { NavigationStack { @@ -73,6 +96,11 @@ struct ContentView: View { } } } + .onChange(of: scenePhase) { newPhase in + if newPhase == .background || newPhase == .inactive { + viewModel.save() + } + } } } diff --git a/MultiChrono/Stopwatch.swift b/MultiChrono/Stopwatch.swift index 56efb1a..6e68a5f 100644 --- a/MultiChrono/Stopwatch.swift +++ b/MultiChrono/Stopwatch.swift @@ -8,8 +8,8 @@ import Foundation import Combine -class Stopwatch: ObservableObject, Identifiable { - let id = UUID() +class Stopwatch: ObservableObject, Identifiable, Codable { + let id: UUID @Published var name: String @Published var elapsedTime: TimeInterval = 0 @Published var isRunning: Bool = false @@ -18,10 +18,44 @@ class Stopwatch: ObservableObject, Identifiable { 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() } @@ -31,8 +65,11 @@ class Stopwatch: ObservableObject, Identifiable { startTime = Date() isRunning = true - - timer = Timer.publish(every: 0.1, on: .main, in: .common) + startTimer() + } + + private func startTimer() { + timer = Timer.publish(every: 0.01, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.tick() @@ -71,12 +108,12 @@ class Stopwatch: ObservableObject, Identifiable { let hours = Int(elapsedTime) / 3600 let minutes = Int(elapsedTime) / 60 % 60 let seconds = Int(elapsedTime) % 60 - let tenths = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 10) + let tenths = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 100) if hours > 0 { - return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths) + return String(format: "%02i:%02i:%02i.%02i", hours, minutes, seconds, tenths) } else { - return String(format: "%02i:%02i.%01i", minutes, seconds, tenths) + return String(format: "%02i:%02i.%02i", minutes, seconds, tenths) } } } diff --git a/MultiChrono/StopwatchRow.swift b/MultiChrono/StopwatchRow.swift index 6397c53..e5f2471 100644 --- a/MultiChrono/StopwatchRow.swift +++ b/MultiChrono/StopwatchRow.swift @@ -38,9 +38,9 @@ struct StopwatchRow: View { .buttonStyle(PlainButtonStyle()) Button(action: onDelete) { - Image(systemName: "trash") + Image(systemName: "trash.circle.fill") .resizable() - .frame(width: 24, height: 24) + .frame(width: 44, height: 44) .foregroundColor(.red) } .buttonStyle(PlainButtonStyle())