From f42de3353c2244ee9b18086b597b21d232846c76 Mon Sep 17 00:00:00 2001 From: Simon Franken Date: Fri, 20 Feb 2026 14:49:44 +0100 Subject: [PATCH] Fix iOS time display and add timer unit labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Times were always showing 0 because ISO8601DateFormatter with default options does not parse fractional seconds, but Prisma/Node.js serialises dates as "2026-02-20T09:00:00.000Z" (with .000). Every date(from:) call silently returned nil, so elapsedTime and duration always fell back to 0. - Date+Extensions: fromISO8601 now tries .withFractionalSeconds first, then falls back to whole seconds — single place to maintain - OngoingTimer.elapsedTime: use Date.fromISO8601() instead of bare formatter - TimeEntry.duration: use Date.fromISO8601() instead of bare formatters - TimerView: add TimerUnitLabels view showing h/min/sec column headers under the monospaced clock digits --- .../Features/Timer/TimerView.swift | 28 +++++++++++++++++++ .../TimeTracker/Models/OngoingTimer.swift | 4 +-- .../TimeTracker/Models/TimeEntry.swift | 6 ++-- .../Shared/Extensions/Date+Extensions.swift | 12 ++++++-- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift b/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift index 2b59d03..9a5ac48 100644 --- a/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift +++ b/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift @@ -44,6 +44,8 @@ struct TimerView: View { .font(.system(size: 64, weight: .light, design: .monospaced)) .foregroundStyle(viewModel.activeTimer != nil ? .primary : .secondary) + TimerUnitLabels(elapsed: viewModel.elapsedTime) + if let project = viewModel.selectedProject { ProjectColorBadge( color: project.color, @@ -123,6 +125,32 @@ struct TimerView: View { } } +/// Displays "h min sec" (or "min sec") column labels aligned under the +/// monospaced timer digits. +private struct TimerUnitLabels: View { + let elapsed: TimeInterval + + private var showHours: Bool { Int(elapsed) >= 3600 } + + var body: some View { + HStack(spacing: 0) { + if showHours { + Text("h") + .frame(width: 64) + Text("min") + .frame(width: 64) + } else { + Text("min") + .frame(width: 64) + } + Text("sec") + .frame(width: 64) + } + .font(.caption) + .foregroundStyle(.secondary) + } +} + struct ProjectPickerSheet: View { let projects: [Project] let selectedProject: Project? diff --git a/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift b/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift index f4b8714..e8dd9b8 100644 --- a/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift +++ b/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift @@ -9,9 +9,7 @@ struct OngoingTimer: Codable, Identifiable, Equatable { let updatedAt: String var elapsedTime: TimeInterval { - guard let start = ISO8601DateFormatter().date(from: startTime) else { - return 0 - } + guard let start = Date.fromISO8601(startTime) else { return 0 } return Date().timeIntervalSince(start) } } diff --git a/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift b/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift index 38cd007..ac54d41 100644 --- a/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift +++ b/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift @@ -11,10 +11,8 @@ struct TimeEntry: Codable, Identifiable, Equatable { let updatedAt: String var duration: TimeInterval { - guard let start = ISO8601DateFormatter().date(from: startTime), - let end = ISO8601DateFormatter().date(from: endTime) else { - return 0 - } + guard let start = Date.fromISO8601(startTime), + let end = Date.fromISO8601(endTime) else { return 0 } return end.timeIntervalSince(start) } } diff --git a/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift b/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift index 21551a7..d7dec6b 100644 --- a/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift +++ b/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift @@ -59,8 +59,14 @@ extension Date { } static func fromISO8601(_ string: String) -> Date? { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter.date(from: string) + // Try with fractional seconds first (e.g. "2026-02-20T09:00:00.000Z" from + // Prisma/Node.js JSON serialisation), then fall back to whole seconds. + let withFractional = ISO8601DateFormatter() + withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = withFractional.date(from: string) { return date } + + let wholeSec = ISO8601DateFormatter() + wholeSec.formatOptions = [.withInternetDateTime] + return wholeSec.date(from: string) } }