Read all-day events correctly, and make the date circle say a date
Three things, one of which is not a watch problem at all. parseDate already treated a date-only string as all-day, but isAllDay came from the JSON flag alone. An event sent as "2026-09-12" with no flag therefore landed at midnight with isAllDay false, and every surface that formats a time put "00:00" in front of it — the widgets as much as the watch. Now derived from both. The date complication showed the day number and nothing else, because .accessoryCircularCapacity renders only currentValueLabel and the month sat in the label closure, where it is never drawn. It now stacks weekday over day number, which is what makes it read as a date rather than a number. The ring only appears while an appointment is running, and then shows how far through it we are. An empty track around an idle day said nothing, so there is no Gauge at all in that case. All-day events do not count as running: one would hold the ring at an arbitrary fraction for twenty-four hours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1a1230a796
commit
6e91eaaaa2
@@ -91,7 +91,14 @@ struct CalEvent: Identifiable, Hashable {
|
||||
else if let n = json["id"] as? Int { id = String(n) }
|
||||
else { return nil }
|
||||
|
||||
let isAllDay = json["allDay"] as? Bool ?? false
|
||||
// A date-only string is an all-day event whether or not the flag says
|
||||
// so — `parseDate` already treats it that way. Trusting the flag alone
|
||||
// is what put "00:00" in front of birthdays everywhere they are shown.
|
||||
let startIsDateOnly: Bool = {
|
||||
let clean = startStr.trimmingCharacters(in: .whitespaces)
|
||||
return clean.count == 10 && !clean.contains("T")
|
||||
}()
|
||||
let isAllDay = (json["allDay"] as? Bool ?? false) || startIsDateOnly
|
||||
let startDate = parseDate(startStr, allDay: isAllDay)
|
||||
let endDate = parseDate(endStr, allDay: isAllDay)
|
||||
guard let s = startDate, let e = endDate else { return nil }
|
||||
|
||||
@@ -67,7 +67,10 @@ struct CountdownComplicationView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Day number and month. No appointment information — a date replacement.
|
||||
/// Weekday and day number, the way a date is actually read. The ring appears
|
||||
/// only while an appointment is running, and then shows how far through it we
|
||||
/// are — an empty track around an idle day is noise, so there is no Gauge at
|
||||
/// all in that case.
|
||||
struct DateComplicationView: View {
|
||||
let entry: WatchComplicationEntry
|
||||
@Environment(\.widgetFamily) private var family
|
||||
@@ -76,26 +79,47 @@ struct DateComplicationView: View {
|
||||
Calendar(identifier: .gregorian).component(.day, from: entry.date)
|
||||
}
|
||||
|
||||
private var weekday: String {
|
||||
WatchComplicationSupport.weekdayAbbreviation(entry.date, language: entry.language)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
Label("\(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) \(day)",
|
||||
systemImage: "calendar")
|
||||
Label("\(weekday) \(day)", systemImage: "calendar")
|
||||
default:
|
||||
// A closed gauge, filling the slot edge to edge — the WidgetKit
|
||||
// equivalent of ClockKit's ClosedGaugeText, which no longer exists.
|
||||
// The ring is not decoration: it drains as the day does.
|
||||
Gauge(value: WatchComplicationSupport.dayRemainingFraction(at: entry.date)) {
|
||||
Text(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language))
|
||||
circular
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var circular: some View {
|
||||
if let running = WatchComplicationSupport.runningEvent(in: entry.snapshot, at: entry.date) {
|
||||
Gauge(value: WatchComplicationSupport.progress(of: running, at: entry.date)) {
|
||||
Text(weekday)
|
||||
} currentValueLabel: {
|
||||
Text("\(day)")
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.6)
|
||||
dateStack
|
||||
}
|
||||
.gaugeStyle(.accessoryCircularCapacity)
|
||||
.widgetAccentable()
|
||||
.eventBezelLabel(entry)
|
||||
} else {
|
||||
ZStack {
|
||||
AccessoryWidgetBackground()
|
||||
dateStack
|
||||
}
|
||||
.eventBezelLabel(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private var dateStack: some View {
|
||||
VStack(spacing: -2) {
|
||||
Text(weekday)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.widgetAccentable()
|
||||
Text("\(day)")
|
||||
.font(.system(size: 23, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,32 @@ enum WatchComplicationSupport {
|
||||
return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count
|
||||
}
|
||||
|
||||
/// The appointment happening right now, if there is one. All-day events are
|
||||
/// excluded on purpose: one of those is "running" for twenty-four hours and
|
||||
/// would hold the ring at some arbitrary fraction all day, which says
|
||||
/// nothing.
|
||||
static func runningEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||
snapshot?.events
|
||||
.filter { !$0.isAllDay && $0.start <= now && $0.end > now }
|
||||
.min { $0.end < $1.end }
|
||||
}
|
||||
|
||||
/// How far through an appointment we are, 0 at its start and 1 at its end.
|
||||
static func progress(of event: SnapshotEvent, at now: Date) -> Double {
|
||||
let total = event.end.timeIntervalSince(event.start)
|
||||
guard total > 0 else { return 0 }
|
||||
return min(1, max(0, now.timeIntervalSince(event.start) / total))
|
||||
}
|
||||
|
||||
static func weekdayAbbreviation(_ date: Date, language: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = WatchL10n.locale(language)
|
||||
formatter.dateFormat = "EEE"
|
||||
return formatter.string(from: date)
|
||||
.replacingOccurrences(of: ".", with: "")
|
||||
.uppercased()
|
||||
}
|
||||
|
||||
/// What a countdown counts to: the start if it has not begun, otherwise the
|
||||
/// end. Counting up from a start time that has passed reads as broken.
|
||||
static func countdownTarget(_ event: SnapshotEvent, at now: Date) -> Date {
|
||||
|
||||
Reference in New Issue
Block a user