implement laps, reset

This commit is contained in:
2026-01-28 23:09:21 +01:00
parent 750a096999
commit a607e33ce9
3 changed files with 159 additions and 18 deletions

View File

@@ -114,6 +114,11 @@ struct ContentView: View {
},
onCancel: {
selectedStopwatch = nil
},
onReset: {
stopwatch.reset()
viewModel.save()
selectedStopwatch = nil
}
)
}

View File

@@ -9,17 +9,26 @@ import Foundation
import Combine
class Stopwatch: ObservableObject, Identifiable, Codable {
struct Lap: Identifiable, Codable {
let id: UUID
let number: Int
let startTime: Date
let endTime: Date
let duration: TimeInterval
}
let id: UUID
@Published var name: String
@Published var elapsedTime: TimeInterval = 0
@Published var isRunning: Bool = false
@Published var laps: [Lap] = []
private var timer: AnyCancellable?
private var startTime: Date?
private var accumulatedTime: TimeInterval = 0
enum CodingKeys: String, CodingKey {
case id, name, elapsedTime, isRunning, startTime, accumulatedTime
case id, name, elapsedTime, isRunning, startTime, accumulatedTime, laps
}
init(name: String) {
@@ -35,6 +44,7 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
isRunning = try container.decode(Bool.self, forKey: .isRunning)
startTime = try container.decodeIfPresent(Date.self, forKey: .startTime)
accumulatedTime = try container.decode(TimeInterval.self, forKey: .accumulatedTime)
laps = try container.decodeIfPresent([Lap].self, forKey: .laps) ?? []
if isRunning {
// Restart the timer if it was running.
@@ -54,6 +64,7 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
try container.encode(isRunning, forKey: .isRunning)
try container.encode(startTime, forKey: .startTime)
try container.encode(accumulatedTime, forKey: .accumulatedTime)
try container.encode(laps, forKey: .laps)
}
deinit {
@@ -82,8 +93,20 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
timer?.cancel()
timer = nil
let now = Date()
if let startTime = startTime {
accumulatedTime += Date().timeIntervalSince(startTime)
let sessionDuration = now.timeIntervalSince(startTime)
accumulatedTime += sessionDuration
// Record Lap
let newLap = Lap(
id: UUID(),
number: laps.count + 1,
startTime: startTime,
endTime: now,
duration: sessionDuration
)
laps.append(newLap)
}
isRunning = false
@@ -94,9 +117,17 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
}
func reset() {
pause()
// Stop timer without recording a lap? Or should reset clear laps?
// Usually reset clears everything.
if isRunning {
timer?.cancel()
timer = nil
isRunning = false
startTime = nil
}
accumulatedTime = 0
elapsedTime = 0
laps.removeAll()
}
private func tick() {
@@ -104,28 +135,28 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
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
static func format(interval: TimeInterval, format: TimeFormat) -> String {
let hours = Int(interval) / 3600
let minutes = Int(interval) / 60 % 60
let seconds = Int(interval) % 60
switch format {
case .millis:
let millis = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 1000)
let millis = Int((interval.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)
let cents = Int((interval.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)
let tenths = Int((interval.truncatingRemainder(dividingBy: 1)) * 10)
if hours > 0 {
return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths)
} else {
@@ -140,8 +171,21 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
}
}
func formattedTime(format: TimeFormat = .cents) -> String {
return Stopwatch.format(interval: elapsedTime, format: format)
}
// Legacy support to avoid breaking existing code immediately
var formattedTime: String {
formattedTime(format: AppSettings.shared.timeFormat)
}
var currentRunDuration: TimeInterval? {
guard let startTime = startTime else { return nil }
return Date().timeIntervalSince(startTime)
}
var currentRunStartTime: Date? {
return startTime
}
}

View File

@@ -8,16 +8,25 @@
import SwiftUI
struct StopwatchDetailView: View {
let stopwatch: Stopwatch
@ObservedObject var stopwatch: Stopwatch
let onSave: (String) -> Void
let onCancel: () -> Void
let onReset: () -> Void
@State private var draftName: String
@State private var isShowingResetAlert = false
init(stopwatch: Stopwatch, onSave: @escaping (String) -> Void, onCancel: @escaping () -> Void) {
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
return formatter
}()
init(stopwatch: Stopwatch, onSave: @escaping (String) -> Void, onCancel: @escaping () -> Void, onReset: @escaping () -> Void) {
self.stopwatch = stopwatch
self.onSave = onSave
self.onCancel = onCancel
self.onReset = onReset
_draftName = State(initialValue: stopwatch.name)
}
@@ -27,9 +36,91 @@ struct StopwatchDetailView: View {
Section(header: Text("Name")) {
TextField("Enter name...", text: $draftName)
}
Section(header: Text("Laps")) {
if stopwatch.isRunning,
let currentStart = stopwatch.currentRunStartTime,
let currentDuration = stopwatch.currentRunDuration {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Lap \(stopwatch.laps.count + 1)")
.font(.headline)
.foregroundStyle(.blue)
Spacer()
Text(Stopwatch.format(interval: currentDuration, format: AppSettings.shared.timeFormat))
.font(.system(.body, design: .monospaced))
.bold()
.foregroundStyle(.blue)
}
HStack {
Text("Current")
.font(.caption)
.foregroundStyle(.blue)
Spacer()
VStack(alignment: .trailing) {
Text(dateFormatter.string(from: currentStart))
Text("Running...")
}
}
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.blue)
}
.padding(.vertical, 2)
}
if stopwatch.laps.isEmpty && !stopwatch.isRunning {
Text("No laps recorded yet.")
.foregroundStyle(.secondary)
} else {
List {
ForEach(stopwatch.laps) { lap in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Lap \(lap.number)")
.font(.headline)
Spacer()
Text(Stopwatch.format(interval: lap.duration, format: AppSettings.shared.timeFormat))
.font(.system(.body, design: .monospaced))
.bold()
}
HStack {
Spacer()
VStack(alignment: .trailing) {
Text(dateFormatter.string(from: lap.startTime))
Text(dateFormatter.string(from: lap.endTime))
}
}
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
}
.padding(.vertical, 2)
}
}
}
}
Section {
Button(role: .destructive) {
isShowingResetAlert = true
} label: {
HStack {
Spacer()
Text("Reset Stopwatch")
Spacer()
}
}
}
}
.navigationTitle("Edit Stopwatch")
.navigationBarTitleDisplayMode(.inline)
.alert("Are you sure you want to reset the Stopwatch \(stopwatch.name)?", isPresented: $isShowingResetAlert) {
Button("Reset", role: .destructive) {
onReset()
}
Button("Cancel", role: .cancel) {}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
@@ -51,6 +142,7 @@ struct StopwatchDetailView: View {
StopwatchDetailView(
stopwatch: Stopwatch(name: "Test Timer"),
onSave: { _ in },
onCancel: {}
onCancel: {},
onReset: {}
)
}