implement lap edit
This commit is contained in:
165
MultiChrono/EditLapView.swift
Normal file
165
MultiChrono/EditLapView.swift
Normal 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 }
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +79,9 @@ struct StopwatchDetailView: View {
|
||||
} else {
|
||||
List {
|
||||
ForEach(stopwatch.laps) { lap in
|
||||
Button {
|
||||
editingLap = lap
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("Lap \(lap.number)")
|
||||
@@ -100,6 +104,8 @@ struct StopwatchDetailView: View {
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,26 +114,21 @@ struct StopwatchDetailView: View {
|
||||
Button(role: .destructive) {
|
||||
isShowingResetAlert = true
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Reset Stopwatch")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
isShowingDeleteAlert = true
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user