Render the circular complications as edge-to-edge closed gauges

Date and "events today" filled their slot with AccessoryWidgetBackground and
centred text, which reads as a small element with space around it. Both are now
Gauges in .accessoryCircularCapacity — the WidgetKit equivalent of ClockKit's
ClosedGauge templates, which no longer exist and cannot be reached from a
WidgetKit complication at all.

The rings carry meaning rather than decorating: the date's drains through the
day, and the count's drains as today's appointments are worked through. Both
drain, matching the countdown rather than inventing a second direction. An
empty day leaves the ring empty instead of full, so it does not read as an
untouched one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-11 18:38:54 +02:00
co-authored by Claude Opus 5
parent b489f10c28
commit 938ca7ca98
2 changed files with 47 additions and 21 deletions
@@ -69,17 +69,19 @@ struct DateComplicationView: View {
Label("\(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) \(day)", Label("\(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) \(day)",
systemImage: "calendar") systemImage: "calendar")
default: default:
ZStack { // A closed gauge, filling the slot edge to edge — the WidgetKit
AccessoryWidgetBackground() // equivalent of ClockKit's ClosedGaugeText, which no longer exists.
VStack(spacing: 0) { // The ring is not decoration: it drains as the day does.
Text("\(day)") Gauge(value: WatchComplicationSupport.dayRemainingFraction(at: entry.date)) {
.font(.system(size: 22, weight: .bold)) Text(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language))
.minimumScaleFactor(0.7) } currentValueLabel: {
.widgetAccentable() Text("\(day)")
Text(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) .font(.system(size: 20, weight: .semibold))
.font(.system(size: 8, weight: .semibold)) .lineLimit(1)
} .minimumScaleFactor(0.6)
} }
.gaugeStyle(.accessoryCircularCapacity)
.widgetAccentable()
} }
} }
} }
@@ -99,18 +101,17 @@ struct TodayCountComplicationView: View {
Label("\(count) \(WatchL10n.t("watch.events_today", entry.language))", Label("\(count) \(WatchL10n.t("watch.events_today", entry.language))",
systemImage: "calendar") systemImage: "calendar")
default: default:
ZStack { Gauge(value: WatchComplicationSupport.todayRemainingFraction(in: entry.snapshot,
AccessoryWidgetBackground() at: entry.date)) {
VStack(spacing: 0) { Image(systemName: "calendar")
Text("\(count)") } currentValueLabel: {
.font(.system(size: 22, weight: .bold)) Text("\(count)")
.widgetAccentable() .font(.system(size: 20, weight: .semibold))
Text(WatchL10n.t("watch.events_today", entry.language).uppercased()) .lineLimit(1)
.font(.system(size: 7, weight: .semibold)) .minimumScaleFactor(0.6)
.lineLimit(1)
.minimumScaleFactor(0.6)
}
} }
.gaugeStyle(.accessoryCircularCapacity)
.widgetAccentable()
} }
} }
} }
@@ -50,6 +50,31 @@ enum WatchComplicationSupport {
return min(1, max(0, target.timeIntervalSince(now) / window)) return min(1, max(0, target.timeIntervalSince(now) / window))
} }
/// How much of today is still ahead. Drains through the day, matching the
/// countdown ring rather than inventing a second direction.
static func dayRemainingFraction(at now: Date,
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
let dayStart = calendar.startOfDay(for: now)
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
let total = dayEnd.timeIntervalSince(dayStart)
guard total > 0 else { return 0 }
return min(1, max(0, dayEnd.timeIntervalSince(now) / total))
}
/// The share of today's appointments still ahead. Zero when there are none,
/// which leaves the ring empty rather than full — an empty day should not
/// look like an untouched one.
static func todayRemainingFraction(in snapshot: CalendarrSnapshot?, at now: Date,
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
guard let snapshot else { return 0 }
let dayStart = calendar.startOfDay(for: now)
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
let today = snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }
guard !today.isEmpty else { return 0 }
let ahead = today.filter { $0.end > now }.count
return min(1, max(0, Double(ahead) / Double(today.count)))
}
static func time(_ date: Date, language: String) -> String { static func time(_ date: Date, language: String) -> String {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.locale = WatchL10n.locale(language) formatter.locale = WatchL10n.locale(language)