From 9ea1a8705d608b1aa476c0b35685c57466bb0c50 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 11 Sep 2026 18:32:51 +0200 Subject: [PATCH] Give complication timelines minute granularity in the first hour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A compact countdown needs an entry per minute to stay honest. The live Text(style: .timer) it replaces needed none, but renders 1:23:45 — eight characters, which does not fit an accessoryCorner beside its curved label. Co-Authored-By: Claude Opus 5 --- .../Complications/ComplicationTimeline.swift | 22 +++++++++++++++---- .../ComplicationTimelineTests.swift | 21 ++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Sources/CalendarrCore/Complications/ComplicationTimeline.swift b/Sources/CalendarrCore/Complications/ComplicationTimeline.swift index 06ae07f..c04aa1e 100644 --- a/Sources/CalendarrCore/Complications/ComplicationTimeline.swift +++ b/Sources/CalendarrCore/Complications/ComplicationTimeline.swift @@ -10,14 +10,22 @@ public enum ComplicationTimeline { /// Dates at which a complication should re-render. /// /// Always begins with `now` so the first entry renders immediately. Event - /// starts and ends within `horizon` become entries; hourly marks fill the - /// rest so a quiet calendar still advances its date and countdown. The - /// result is sorted, deduplicated to the second, and capped at + /// starts and ends within `horizon` become entries. + /// + /// The first `fineGrainedWindow` then gets a mark every minute, and the rest + /// of the horizon one every hour. The fine-grained part is what lets a + /// complication show a *compact* countdown — "23m" rather than "1:23:45" — + /// which is the only kind that fits an accessoryCorner or a circular gauge. + /// A live `Text(style: .timer)` needs no entries at all but is eight + /// characters wide, and overlaps the curved label it sits next to. + /// + /// The result is sorted, deduplicated to the second, and capped at /// `maxEntries`, keeping the earliest. public static func entryDates(from now: Date, in snapshot: CalendarrSnapshot, horizon: TimeInterval = 24 * 3600, - maxEntries: Int = 100) -> [Date] { + fineGrainedWindow: TimeInterval = 3600, + maxEntries: Int = 150) -> [Date] { let end = now.addingTimeInterval(horizon) var seen: Set = [] var dates: [Date] = [] @@ -36,6 +44,12 @@ public enum ComplicationTimeline { add(event.start) add(event.end) } + let fineEnd = min(end, now.addingTimeInterval(fineGrainedWindow)) + var minute = now.addingTimeInterval(60) + while minute <= fineEnd { + add(minute) + minute = minute.addingTimeInterval(60) + } var hourly = now.addingTimeInterval(3600) while hourly <= end { add(hourly) diff --git a/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift b/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift index bfe1594..eea5e88 100644 --- a/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift +++ b/Tests/CalendarrCoreTests/ComplicationTimelineTests.swift @@ -73,6 +73,27 @@ struct ComplicationTimelineTests { #expect(dates.count >= 24) } + @Test("The first hour gets minute-granular entries, so a compact countdown stays honest") + func minuteGranularFirstHour() { + let dates = ComplicationTimeline.entryDates(from: Self.now, in: Self.snapshot([])) + let firstHour = dates.filter { $0 <= Self.now.addingTimeInterval(3600) } + + // 60 minute marks plus `now` itself. + #expect(firstHour.count >= 60) + #expect(firstHour.contains(Self.now.addingTimeInterval(60))) + #expect(firstHour.contains(Self.now.addingTimeInterval(1800))) + } + + @Test("Past the first hour the entries thin out to hourly") + func hourlyAfterFirstHour() { + let dates = ComplicationTimeline.entryDates(from: Self.now, in: Self.snapshot([])) + let later = dates.filter { $0 > Self.now.addingTimeInterval(3600) } + + // 23 further hours, not 23 * 60 minutes. + #expect(later.count < 40) + #expect(later.contains(Self.now.addingTimeInterval(2 * 3600))) + } + @Test("The entry count is capped, keeping the earliest entries") func capsEntries() { var events: [SnapshotEvent] = []