From a3b73e101b66ac055a33c3e6efc4040b99d5e9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Tue, 27 Jan 2026 01:44:33 +0100 Subject: [PATCH] implement multi stopwatch --- MultiChrono.xcodeproj/project.pbxproj | 2 + MultiChrono/AddStopwatchView.swift | 40 +++++++++++++ MultiChrono/ContentView.swift | 69 ++++++++++++++++++++-- MultiChrono/Stopwatch.swift | 82 +++++++++++++++++++++++++++ MultiChrono/StopwatchRow.swift | 56 ++++++++++++++++++ 5 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 MultiChrono/AddStopwatchView.swift create mode 100644 MultiChrono/Stopwatch.swift create mode 100644 MultiChrono/StopwatchRow.swift diff --git a/MultiChrono.xcodeproj/project.pbxproj b/MultiChrono.xcodeproj/project.pbxproj index 9e97d84..9301143 100644 --- a/MultiChrono.xcodeproj/project.pbxproj +++ b/MultiChrono.xcodeproj/project.pbxproj @@ -251,6 +251,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5X22UJP4XP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -282,6 +283,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5X22UJP4XP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/MultiChrono/AddStopwatchView.swift b/MultiChrono/AddStopwatchView.swift new file mode 100644 index 0000000..6812348 --- /dev/null +++ b/MultiChrono/AddStopwatchView.swift @@ -0,0 +1,40 @@ +// +// AddStopwatchView.swift +// MultiChrono +// +// Created by Beatrice Dellacà on 26/01/26. +// + +import SwiftUI + +struct AddStopwatchView: View { + @Environment(\.dismiss) var dismiss + @State private var name: String = "" + var onAdd: (String) -> Void + + var body: some View { + NavigationStack { + Form { + TextField("Stopwatch Name", text: $name) + } + .navigationTitle("New Stopwatch") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Add") { + onAdd(name.isEmpty ? "Stopwatch" : name) + dismiss() + } + } + } + } + } +} + +#Preview { + AddStopwatchView(onAdd: { _ in }) +} diff --git a/MultiChrono/ContentView.swift b/MultiChrono/ContentView.swift index 52ede7c..0a23971 100644 --- a/MultiChrono/ContentView.swift +++ b/MultiChrono/ContentView.swift @@ -6,16 +6,73 @@ // import SwiftUI +import Combine + +class ContentViewModel: ObservableObject { + @Published var stopwatches: [Stopwatch] = [] + + func addStopwatch(name: String) { + let newStopwatch = Stopwatch(name: name) + stopwatches.append(newStopwatch) + } + + func deleteStopwatch(at index: Int) { + stopwatches[index].pause() // Ensure timer is stopped + stopwatches.remove(at: index) + } + + func deleteStopwatch(id: UUID) { + if let index = stopwatches.firstIndex(where: { $0.id == id }) { + deleteStopwatch(at: index) + } + } +} struct ContentView: View { + @StateObject private var viewModel = ContentViewModel() + @State private var isShowingAddSheet = false + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + NavigationStack { + List { + ForEach(viewModel.stopwatches) { stopwatch in + StopwatchRow(stopwatch: stopwatch, onDelete: { + viewModel.deleteStopwatch(id: stopwatch.id) + }) + } + .onDelete { indexSet in + for index in indexSet { + viewModel.deleteStopwatch(at: index) + } + } + } + .listStyle(.plain) + .navigationTitle("MultiChrono") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: { + isShowingAddSheet = true + }) { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $isShowingAddSheet) { + AddStopwatchView { name in + viewModel.addStopwatch(name: name) + isShowingAddSheet = false + } + } + .overlay { + if viewModel.stopwatches.isEmpty { + ContentUnavailableView( + "No Stopwatches", + systemImage: "timer", + description: Text("Tap the + button to create a stopwatch.") + ) + } + } } - .padding() } } diff --git a/MultiChrono/Stopwatch.swift b/MultiChrono/Stopwatch.swift new file mode 100644 index 0000000..56efb1a --- /dev/null +++ b/MultiChrono/Stopwatch.swift @@ -0,0 +1,82 @@ +// +// Stopwatch.swift +// MultiChrono +// +// Created by Beatrice Dellacà on 26/01/26. +// + +import Foundation +import Combine + +class Stopwatch: ObservableObject, Identifiable { + let id = UUID() + @Published var name: String + @Published var elapsedTime: TimeInterval = 0 + @Published var isRunning: Bool = false + + private var timer: AnyCancellable? + private var startTime: Date? + private var accumulatedTime: TimeInterval = 0 + + init(name: String) { + self.name = name + } + + deinit { + timer?.cancel() + } + + func start() { + guard !isRunning else { return } + + startTime = Date() + isRunning = true + + timer = Timer.publish(every: 0.1, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.tick() + } + } + + func pause() { + guard isRunning else { return } + + timer?.cancel() + timer = nil + + if let startTime = startTime { + accumulatedTime += Date().timeIntervalSince(startTime) + } + + isRunning = false + startTime = nil + + // Ensure UI shows accurate accumulated time + elapsedTime = accumulatedTime + } + + func reset() { + pause() + accumulatedTime = 0 + elapsedTime = 0 + } + + private func tick() { + guard let startTime = startTime else { return } + elapsedTime = accumulatedTime + Date().timeIntervalSince(startTime) + } + + var formattedTime: String { + let hours = Int(elapsedTime) / 3600 + let minutes = Int(elapsedTime) / 60 % 60 + let seconds = Int(elapsedTime) % 60 + let tenths = Int((elapsedTime.truncatingRemainder(dividingBy: 1)) * 10) + + if hours > 0 { + return String(format: "%02i:%02i:%02i.%01i", hours, minutes, seconds, tenths) + } else { + return String(format: "%02i:%02i.%01i", minutes, seconds, tenths) + } + } +} diff --git a/MultiChrono/StopwatchRow.swift b/MultiChrono/StopwatchRow.swift new file mode 100644 index 0000000..6397c53 --- /dev/null +++ b/MultiChrono/StopwatchRow.swift @@ -0,0 +1,56 @@ +// +// StopwatchRow.swift +// MultiChrono +// +// Created by Beatrice Dellacà on 26/01/26. +// + +import SwiftUI + +struct StopwatchRow: View { + @ObservedObject var stopwatch: Stopwatch + var onDelete: () -> Void + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(stopwatch.name) + .font(.headline) + Text("\(stopwatch.formattedTime)") + .font(.largeTitle) + .monospacedDigit() + } + + Spacer() + + Button(action: { + if stopwatch.isRunning { + stopwatch.pause() + } else { + stopwatch.start() + } + }) { + Image(systemName: stopwatch.isRunning ? "pause.circle.fill" : "play.circle.fill") + .resizable() + .frame(width: 44, height: 44) + .foregroundStyle(stopwatch.isRunning ? .orange : .green) + } + .buttonStyle(PlainButtonStyle()) + + Button(action: onDelete) { + Image(systemName: "trash") + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.red) + } + .buttonStyle(PlainButtonStyle()) + .padding(.leading, 10) + + } + .padding(.vertical, 8) + } +} + +#Preview { + StopwatchRow(stopwatch: Stopwatch(name: "Test Timer"), onDelete: {}) +}