implement multi stopwatch
This commit is contained in:
@@ -251,6 +251,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 5X22UJP4XP;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
@@ -282,6 +283,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 5X22UJP4XP;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = 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 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 {
|
struct ContentView: View {
|
||||||
|
@StateObject private var viewModel = ContentViewModel()
|
||||||
|
@State private var isShowingAddSheet = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
NavigationStack {
|
||||||
Image(systemName: "globe")
|
List {
|
||||||
.imageScale(.large)
|
ForEach(viewModel.stopwatches) { stopwatch in
|
||||||
.foregroundStyle(.tint)
|
StopwatchRow(stopwatch: stopwatch, onDelete: {
|
||||||
Text("Hello, world!")
|
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