From 190cfc40717326106120ac5c70e2f8be96726eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Thu, 5 Feb 2026 00:54:58 +0100 Subject: [PATCH] implement lap edit --- MultiChrono/EditLapView.swift | 165 ++++++++++++++++++++++++++ MultiChrono/Stopwatch.swift | 36 +++++- MultiChrono/StopwatchDetailView.swift | 66 ++++++----- 3 files changed, 234 insertions(+), 33 deletions(-) create mode 100644 MultiChrono/EditLapView.swift diff --git a/MultiChrono/EditLapView.swift b/MultiChrono/EditLapView.swift new file mode 100644 index 0000000..b757795 --- /dev/null +++ b/MultiChrono/EditLapView.swift @@ -0,0 +1,165 @@ +// +// EditLapView.swift +// MultiChrono +// +// Created by Beatrice DellacĂ  on 27/01/26. +// + +import SwiftUI + +struct EditLapView: View { + @Environment(\.dismiss) var dismiss + @State var lap: Stopwatch.Lap + var onSave: (Stopwatch.Lap) -> Void + + var body: some View { + NavigationStack { + Form { + Section(header: Text("Start Time")) { + PreciseTimePicker(date: $lap.startTime) + } + + Section(header: Text("End Time")) { + PreciseTimePicker(date: $lap.endTime) + } + + Section(header: Text("Duration")) { + HStack { + Text("Duration") + Spacer() + Text(Stopwatch.format(interval: lap.endTime.timeIntervalSince(lap.startTime), format: AppSettings.shared.timeFormat)) + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("Edit Lap \(lap.number)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + onSave(lap) + dismiss() + } + } + } + } + } +} + +struct PreciseTimePicker: View { + @Binding var date: Date + + var body: some View { + VStack(spacing: 12) { + DatePicker("Date & Time", selection: $date, displayedComponents: [.hourAndMinute, .date]) + + HStack(spacing: 0) { + Text("Sec") + .font(.caption) + .frame(width: 40) + + Picker("Seconds", selection: Binding( + get: { Calendar.current.component(.second, from: date) }, + set: { newSecond in + if let newDate = Calendar.current.date(bySetting: .second, value: newSecond, of: date) { + date = newDate + } + } + )) { + ForEach(0..<60) { + Text(String(format: "%02d", $0)).tag($0) + } + } + .pickerStyle(.wheel) + .frame(height: 100) + .clipped() + + Spacer() + .frame(width: 20) + + Text("ms") + .font(.caption) + .frame(width: 30) + + // Milliseconds - Hundreds + Picker("Hundreds", selection: Binding( + get: { (Calendar.current.component(.nanosecond, from: date) / 1_000_000) / 100 }, + set: { newHundreds in + let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000 + let tensAndUnits = currentMillis % 100 + let totalMillis = (newHundreds * 100) + tensAndUnits + if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) { + date = newDate + } + } + )) { + ForEach(0..<10) { + Text("\($0)").tag($0) + } + } + .pickerStyle(.wheel) + .frame(width: 40, height: 100) + .clipped() + + // Milliseconds - Tens + Picker("Tens", selection: Binding( + get: { ((Calendar.current.component(.nanosecond, from: date) / 1_000_000) % 100) / 10 }, + set: { newTens in + let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000 + let hundreds = currentMillis / 100 + let units = currentMillis % 10 + let totalMillis = (hundreds * 100) + (newTens * 10) + units + if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) { + date = newDate + } + } + )) { + ForEach(0..<10) { + Text("\($0)").tag($0) + } + } + .pickerStyle(.wheel) + .frame(width: 40, height: 100) + .clipped() + + // Milliseconds - Units + Picker("Units", selection: Binding( + get: { (Calendar.current.component(.nanosecond, from: date) / 1_000_000) % 10 }, + set: { newUnits in + let currentMillis = Calendar.current.component(.nanosecond, from: date) / 1_000_000 + let hundredsAndTens = (currentMillis / 10) * 10 + let totalMillis = hundredsAndTens + newUnits + if let newDate = Calendar.current.date(bySetting: .nanosecond, value: totalMillis * 1_000_000, of: date) { + date = newDate + } + } + )) { + ForEach(0..<10) { + Text("\($0)").tag($0) + } + } + .pickerStyle(.wheel) + .frame(width: 40, height: 100) + .clipped() + } + } + } +} + +#Preview { + EditLapView( + lap: Stopwatch.Lap( + id: UUID(), + number: 1, + startTime: Date(), + endTime: Date().addingTimeInterval(65), + duration: 65 + ), + onSave: { _ in } + ) +} diff --git a/MultiChrono/Stopwatch.swift b/MultiChrono/Stopwatch.swift index 99b187a..a596dc9 100644 --- a/MultiChrono/Stopwatch.swift +++ b/MultiChrono/Stopwatch.swift @@ -12,9 +12,9 @@ class Stopwatch: ObservableObject, Identifiable, Codable { struct Lap: Identifiable, Codable { let id: UUID let number: Int - let startTime: Date - let endTime: Date - let duration: TimeInterval + var startTime: Date + var endTime: Date + var duration: TimeInterval } let id: UUID @@ -188,4 +188,34 @@ class Stopwatch: ObservableObject, Identifiable, Codable { var currentRunStartTime: Date? { return startTime } + + func update(lap: Lap) { + guard let index = laps.firstIndex(where: { $0.id == lap.id }) else { return } + + var updatedLap = lap + // Recalculate duration in case start/end times were changed + updatedLap.duration = updatedLap.endTime.timeIntervalSince(updatedLap.startTime) + + laps[index] = updatedLap + + // Recalculate accumulatedTime from all laps + // Note: This assumes accumulatedTime is strictly the sum of all closed laps. + // If the stopwatch is running, the current "open" lap is not in `laps` yet (it's implicit). + // However, `accumulatedTime` tracks the time *before* the current run started. + // If we edited a *past* lap, we should update accumulatedTime to be the sum of all past laps. + + // Let's assume laps are only recorded when paused or when split. + // If we claim `accumulatedTime` is the sum of all stored laps: + let totalLapDuration = laps.reduce(0) { $0 + $1.duration } + accumulatedTime = totalLapDuration + + if isRunning { + // function tick() will update elapsedTime naturally, but to be immediate: + if let start = startTime { + elapsedTime = accumulatedTime + Date().timeIntervalSince(start) + } + } else { + elapsedTime = accumulatedTime + } + } } diff --git a/MultiChrono/StopwatchDetailView.swift b/MultiChrono/StopwatchDetailView.swift index 2ecdc14..ea15ef5 100644 --- a/MultiChrono/StopwatchDetailView.swift +++ b/MultiChrono/StopwatchDetailView.swift @@ -17,6 +17,7 @@ struct StopwatchDetailView: View { @State private var draftName: String @State private var isShowingResetAlert = false @State private var isShowingDeleteAlert = false + @State private var editingLap: Stopwatch.Lap? private let dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -78,27 +79,32 @@ struct StopwatchDetailView: View { } 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)) + Button { + editingLap = lap + } label: { + 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) } - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(.secondary) + .padding(.vertical, 2) } - .padding(.vertical, 2) + .buttonStyle(.plain) } } } @@ -108,26 +114,21 @@ struct StopwatchDetailView: View { Button(role: .destructive) { isShowingResetAlert = true } label: { - HStack { - Spacer() - Text("Reset Stopwatch") - Spacer() - } + Text("Reset Stopwatch") } + .frame(maxWidth: .infinity, alignment: .center) } Section { - Button(role: .destructive) { - isShowingDeleteAlert = true - } label: { - HStack { - Spacer() + Button(role: .destructive) { + isShowingDeleteAlert = true + } label: { Text("Delete Stopwatch") - Spacer() } + .frame(maxWidth: .infinity, alignment: .center) } } - } + .navigationTitle("Edit Stopwatch") .navigationBarTitleDisplayMode(.inline) .alert("Are you sure you want to reset the Stopwatch \(stopwatch.name)?", isPresented: $isShowingResetAlert) { @@ -155,6 +156,11 @@ struct StopwatchDetailView: View { .disabled(draftName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } } + .sheet(item: $editingLap) { lap in + EditLapView(lap: lap) { updatedLap in + stopwatch.update(lap: updatedLap) + } + } } } }