implement lap edit

This commit is contained in:
2026-02-05 00:54:58 +01:00
parent 93cf6f8483
commit 190cfc4071
3 changed files with 234 additions and 33 deletions

View File

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

View File

@@ -12,9 +12,9 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
struct Lap: Identifiable, Codable { struct Lap: Identifiable, Codable {
let id: UUID let id: UUID
let number: Int let number: Int
let startTime: Date var startTime: Date
let endTime: Date var endTime: Date
let duration: TimeInterval var duration: TimeInterval
} }
let id: UUID let id: UUID
@@ -188,4 +188,34 @@ class Stopwatch: ObservableObject, Identifiable, Codable {
var currentRunStartTime: Date? { var currentRunStartTime: Date? {
return startTime 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
}
}
} }

View File

@@ -17,6 +17,7 @@ struct StopwatchDetailView: View {
@State private var draftName: String @State private var draftName: String
@State private var isShowingResetAlert = false @State private var isShowingResetAlert = false
@State private var isShowingDeleteAlert = false @State private var isShowingDeleteAlert = false
@State private var editingLap: Stopwatch.Lap?
private let dateFormatter: DateFormatter = { private let dateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
@@ -78,27 +79,32 @@ struct StopwatchDetailView: View {
} else { } else {
List { List {
ForEach(stopwatch.laps) { lap in ForEach(stopwatch.laps) { lap in
VStack(alignment: .leading, spacing: 4) { Button {
HStack { editingLap = lap
Text("Lap \(lap.number)") } label: {
.font(.headline) VStack(alignment: .leading, spacing: 4) {
Spacer() HStack {
Text(Stopwatch.format(interval: lap.duration, format: AppSettings.shared.timeFormat)) Text("Lap \(lap.number)")
.font(.system(.body, design: .monospaced)) .font(.headline)
.bold() Spacer()
} Text(Stopwatch.format(interval: lap.duration, format: AppSettings.shared.timeFormat))
.font(.system(.body, design: .monospaced))
HStack { .bold()
Spacer()
VStack(alignment: .trailing) {
Text(dateFormatter.string(from: lap.startTime))
Text(dateFormatter.string(from: lap.endTime))
} }
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)) .padding(.vertical, 2)
.foregroundStyle(.secondary)
} }
.padding(.vertical, 2) .buttonStyle(.plain)
} }
} }
} }
@@ -108,26 +114,21 @@ struct StopwatchDetailView: View {
Button(role: .destructive) { Button(role: .destructive) {
isShowingResetAlert = true isShowingResetAlert = true
} label: { } label: {
HStack { Text("Reset Stopwatch")
Spacer()
Text("Reset Stopwatch")
Spacer()
}
} }
.frame(maxWidth: .infinity, alignment: .center)
} }
Section { Section {
Button(role: .destructive) { Button(role: .destructive) {
isShowingDeleteAlert = true isShowingDeleteAlert = true
} label: { } label: {
HStack {
Spacer()
Text("Delete Stopwatch") Text("Delete Stopwatch")
Spacer()
} }
.frame(maxWidth: .infinity, alignment: .center)
} }
} }
}
.navigationTitle("Edit Stopwatch") .navigationTitle("Edit Stopwatch")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.alert("Are you sure you want to reset the Stopwatch \(stopwatch.name)?", isPresented: $isShowingResetAlert) { .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) .disabled(draftName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
} }
} }
.sheet(item: $editingLap) { lap in
EditLapView(lap: lap) { updatedLap in
stopwatch.update(lap: updatedLap)
}
}
} }
} }
} }