From fe2e8275f56e306b1b44c58dbf513c2980fab707 Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 1 Dec 2025 18:09:16 -0600 Subject: [PATCH] Fix iOS widget date formatting for RFC3339 dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add centralized formatWidgetDate() helper that handles both yyyy-MM-dd and ISO8601 formats - Update widget date display to show "Today", "in X days", or "X days ago" - Fix isTaskOverdue() to parse ISO8601 dates from Go API - Remove duplicate date formatting functions from widget views - Switch API environment back to DEV 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../com/example/casera/network/ApiConfig.kt | 2 +- iosApp/Casera/MyCrib.swift | 123 ++++++++---------- iosApp/iosApp/Helpers/WidgetDataManager.swift | 25 +++- 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt index 45cffa3..a19e50e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/ApiConfig.kt @@ -9,7 +9,7 @@ package com.example.casera.network */ object ApiConfig { // ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️ - val CURRENT_ENV = Environment.LOCAL + val CURRENT_ENV = Environment.DEV enum class Environment { LOCAL, diff --git a/iosApp/Casera/MyCrib.swift b/iosApp/Casera/MyCrib.swift index a780992..77a4e12 100644 --- a/iosApp/Casera/MyCrib.swift +++ b/iosApp/Casera/MyCrib.swift @@ -8,6 +8,53 @@ import WidgetKit import SwiftUI +// MARK: - Date Formatting Helper +/// Parses date strings in either yyyy-MM-dd or ISO8601 (RFC3339) format +/// and returns a user-friendly string like "Today" or "in X days" +private func formatWidgetDate(_ dateString: String) -> String { + let calendar = Calendar.current + var date: Date? + + // Try parsing as yyyy-MM-dd first + let dateOnlyFormatter = DateFormatter() + dateOnlyFormatter.dateFormat = "yyyy-MM-dd" + date = dateOnlyFormatter.date(from: dateString) + + // Try parsing as ISO8601 (RFC3339) if that fails + if date == nil { + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + date = isoFormatter.date(from: dateString) + + // Try without fractional seconds + if date == nil { + isoFormatter.formatOptions = [.withInternetDateTime] + date = isoFormatter.date(from: dateString) + } + } + + guard let parsedDate = date else { + return dateString + } + + let today = calendar.startOfDay(for: Date()) + let dueDay = calendar.startOfDay(for: parsedDate) + + if calendar.isDateInToday(parsedDate) { + return "Today" + } + + let components = calendar.dateComponents([.day], from: today, to: dueDay) + let days = components.day ?? 0 + + if days > 0 { + return days == 1 ? "in 1 day" : "in \(days) days" + } else { + let overdueDays = abs(days) + return overdueDays == 1 ? "1 day ago" : "\(overdueDays) days ago" + } +} + /// CacheManager reads tasks from the App Group shared container /// Data is written by the main app via WidgetDataManager class CacheManager { @@ -192,7 +239,7 @@ struct SmallWidgetView: View { HStack(spacing: 4) { Image(systemName: "calendar") .font(.system(size: 9)) - Text(formatDate(dueDate)) + Text(formatWidgetDate(dueDate)) .font(.system(size: 10, weight: .medium)) } .foregroundStyle(.orange) @@ -225,27 +272,6 @@ struct SmallWidgetView: View { } .padding(16) } - - private func formatDate(_ dateString: String) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - - if let date = formatter.date(from: dateString) { - let now = Date() - let calendar = Calendar.current - - if calendar.isDateInToday(date) { - return "Today" - } else if calendar.isDateInTomorrow(date) { - return "Tomorrow" - } else { - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } - } - - return dateString - } } // MARK: - Medium Widget View @@ -338,15 +364,15 @@ struct TaskRowView: View { HStack(spacing: 3) { Image(systemName: "calendar") .font(.system(size: 8)) - Text(formatDate(dueDate)) + Text(formatWidgetDate(dueDate)) .font(.system(size: 9, weight: .medium)) } .foregroundStyle(.secondary) } } - + Spacer() - + if let priority = task.priority { Text(priority.prefix(1).uppercased()) .font(.system(size: 9, weight: .bold)) @@ -360,7 +386,7 @@ struct TaskRowView: View { } .padding(.vertical, 4) } - + private var priorityColor: Color { switch task.priority?.lowercased() { case "urgent": return .red @@ -369,27 +395,6 @@ struct TaskRowView: View { default: return .gray } } - - private func formatDate(_ dateString: String) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - - if let date = formatter.date(from: dateString) { - let now = Date() - let calendar = Calendar.current - - if calendar.isDateInToday(date) { - return "Today" - } else if calendar.isDateInTomorrow(date) { - return "Tomorrow" - } else { - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } - } - - return dateString - } } // MARK: - Large Widget View @@ -483,7 +488,7 @@ struct LargeTaskRowView: View { HStack(spacing: 2) { Image(systemName: "calendar") .font(.system(size: 7)) - Text(formatDate(dueDate)) + Text(formatWidgetDate(dueDate)) .font(.system(size: 9, weight: isOverdue ? .semibold : .regular)) } .foregroundStyle(isOverdue ? .red : .secondary) @@ -518,28 +523,6 @@ struct LargeTaskRowView: View { default: return .gray } } - - private func formatDate(_ dateString: String) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - - if let date = formatter.date(from: dateString) { - let calendar = Calendar.current - - if calendar.isDateInToday(date) { - return "Today" - } else if calendar.isDateInTomorrow(date) { - return "Tomorrow" - } else if calendar.isDateInYesterday(date) { - return "Yesterday" - } else { - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } - } - - return dateString - } } struct Casera: Widget { diff --git a/iosApp/iosApp/Helpers/WidgetDataManager.swift b/iosApp/iosApp/Helpers/WidgetDataManager.swift index 3d493c7..f30dde3 100644 --- a/iosApp/iosApp/Helpers/WidgetDataManager.swift +++ b/iosApp/iosApp/Helpers/WidgetDataManager.swift @@ -150,15 +150,32 @@ final class WidgetDataManager { private func isTaskOverdue(dueDate: String?, status: String?) -> Bool { guard let dueDateStr = dueDate else { return false } - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" + var date: Date? - guard let date = formatter.date(from: dueDateStr) else { return false } + // Try parsing as yyyy-MM-dd first + let dateOnlyFormatter = DateFormatter() + dateOnlyFormatter.dateFormat = "yyyy-MM-dd" + date = dateOnlyFormatter.date(from: dueDateStr) + + // Try parsing as ISO8601 (RFC3339) if that fails + if date == nil { + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + date = isoFormatter.date(from: dueDateStr) + + // Try without fractional seconds + if date == nil { + isoFormatter.formatOptions = [.withInternetDateTime] + date = isoFormatter.date(from: dueDateStr) + } + } + + guard let parsedDate = date else { return false } // Task is overdue if due date is in the past and status is not completed let statusLower = status?.lowercased() ?? "" let isCompleted = statusLower == "completed" || statusLower == "done" - return !isCompleted && date < Calendar.current.startOfDay(for: Date()) + return !isCompleted && parsedDate < Calendar.current.startOfDay(for: Date()) } }