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)",
systemImage: "calendar")
default:
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Text("\(day)")
.font(.system(size: 22, weight: .bold))
.minimumScaleFactor(0.7)
.widgetAccentable()
// 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))
.font(.system(size: 8, weight: .semibold))
}
} currentValueLabel: {
Text("\(day)")
.font(.system(size: 20, 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))",
systemImage: "calendar")
default:
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Gauge(value: WatchComplicationSupport.todayRemainingFraction(in: entry.snapshot,
at: entry.date)) {
Image(systemName: "calendar")
} currentValueLabel: {
Text("\(count)")
.font(.system(size: 22, weight: .bold))
.widgetAccentable()
Text(WatchL10n.t("watch.events_today", entry.language).uppercased())
.font(.system(size: 7, weight: .semibold))
.font(.system(size: 20, weight: .semibold))
.lineLimit(1)
.minimumScaleFactor(0.6)
}
}
.gaugeStyle(.accessoryCircularCapacity)
.widgetAccentable()
}
}
}
@@ -50,6 +50,31 @@ enum WatchComplicationSupport {
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 {
let formatter = DateFormatter()
formatter.locale = WatchL10n.locale(language)