implement multi stopwatch

This commit is contained in:
2026-01-27 01:44:33 +01:00
parent 4291099fba
commit a3b73e101b
5 changed files with 243 additions and 6 deletions

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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