implement multi stopwatch
This commit is contained in:
@@ -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;
|
||||
|
||||
40
MultiChrono/AddStopwatchView.swift
Normal file
40
MultiChrono/AddStopwatchView.swift
Normal 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 })
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
82
MultiChrono/Stopwatch.swift
Normal file
82
MultiChrono/Stopwatch.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
MultiChrono/StopwatchRow.swift
Normal file
56
MultiChrono/StopwatchRow.swift
Normal 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: {})
|
||||
}
|
||||
Reference in New Issue
Block a user