Archived
Add the watch app and its eight complications
The watch receives the phone's payload over WatchConnectivity and writes it into its own App Group container through the same SnapshotStore, so there is one wire format and one reader. The agenda, the detail screen and the filter all read that; the complications read what the app last wrote, since an extension cannot run a WCSession of its own. Complication timelines land on event boundaries rather than a schedule, so the next appointment flips in the exact minute it changes without spending refresh budget. CalendarrWatchShared holds what both watch targets need, attached to each the way Shared/ is attached to the app and the widgets. recommendations() is implemented because watchOS requires it — iOS has a default, the watch does not, since the complication gallery has to be populated. Found by building, not by reading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a7afe0647
commit
f9bc1965a5
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
import CalendarrCore
|
||||
|
||||
enum WatchComplicationSupport {
|
||||
/// The first event that has not ended yet — including one running now,
|
||||
/// which is what "what is next" means when you are already in a meeting.
|
||||
/// Matches `WatchPushPolicy.nextEventIdentity`, deliberately.
|
||||
static func nextEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||
snapshot?.events.filter { $0.end > now }.min { $0.start < $1.start }
|
||||
}
|
||||
|
||||
static func upcoming(in snapshot: CalendarrSnapshot?, at now: Date, limit: Int) -> [SnapshotEvent] {
|
||||
guard let snapshot else { return [] }
|
||||
return Array(snapshot.events.filter { $0.end > now }.sorted { $0.start < $1.start }.prefix(limit))
|
||||
}
|
||||
|
||||
static func todayCount(in snapshot: CalendarrSnapshot?, at now: Date,
|
||||
calendar: Calendar = Calendar(identifier: .gregorian)) -> Int {
|
||||
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 }
|
||||
return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
event.start > now ? event.start : event.end
|
||||
}
|
||||
|
||||
/// How full a draining ring should be, over a two-hour lead window.
|
||||
static func gaugeFraction(to target: Date, at now: Date) -> Double {
|
||||
let window: TimeInterval = 2 * 3600
|
||||
return min(1, max(0, target.timeIntervalSince(now) / window))
|
||||
}
|
||||
|
||||
static func time(_ date: Date, language: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = WatchL10n.locale(language)
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
static func monthAbbreviation(_ date: Date, language: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = WatchL10n.locale(language)
|
||||
formatter.dateFormat = "LLL"
|
||||
return formatter.string(from: date).uppercased()
|
||||
}
|
||||
|
||||
/// The curved bezel label for a corner complication: time plus title, or a
|
||||
/// plain "no events".
|
||||
static func cornerLabel(_ event: SnapshotEvent?, at now: Date, language: String) -> String {
|
||||
guard let event else { return WatchL10n.t("watch.no_events", language) }
|
||||
if event.isAllDay { return event.title }
|
||||
return "\(time(event.start, language: language)) \(event.title)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user