Archived
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>
63 lines
2.7 KiB
Swift
63 lines
2.7 KiB
Swift
import WidgetKit
|
|
import AppIntents
|
|
import SwiftUI
|
|
import CalendarrCore
|
|
|
|
struct WatchComplicationEntry: TimelineEntry {
|
|
let date: Date
|
|
let snapshot: CalendarrSnapshot?
|
|
|
|
var language: String { snapshot?.language ?? "system" }
|
|
}
|
|
|
|
/// Builds every Calendarr complication's timeline.
|
|
///
|
|
/// Entries land on event boundaries rather than on a fixed schedule, so the
|
|
/// complication flips in the exact minute the next appointment changes without
|
|
/// a network call and without spending refresh budget. New *data* arrives by
|
|
/// push from the phone, which is why the refresh policy is `.atEnd`: when the
|
|
/// timeline simply runs out, regenerating from the same snapshot is cheap and
|
|
/// correct.
|
|
struct WatchComplicationProvider: AppIntentTimelineProvider {
|
|
typealias Entry = WatchComplicationEntry
|
|
typealias Intent = WatchCalendarSelectionIntent
|
|
|
|
func placeholder(in context: Context) -> WatchComplicationEntry {
|
|
WatchComplicationEntry(date: .now, snapshot: nil)
|
|
}
|
|
|
|
func snapshot(for configuration: WatchCalendarSelectionIntent,
|
|
in context: Context) async -> WatchComplicationEntry {
|
|
WatchComplicationEntry(date: .now, snapshot: resolved(configuration))
|
|
}
|
|
|
|
func timeline(for configuration: WatchCalendarSelectionIntent,
|
|
in context: Context) async -> Timeline<WatchComplicationEntry> {
|
|
let snapshot = resolved(configuration)
|
|
let now = Date()
|
|
let dates = snapshot.map { ComplicationTimeline.entryDates(from: now, in: $0) } ?? [now]
|
|
return Timeline(entries: dates.map { WatchComplicationEntry(date: $0, snapshot: snapshot) },
|
|
policy: .atEnd)
|
|
}
|
|
|
|
/// What the complication gallery offers before anyone configures anything.
|
|
/// Required on watchOS — iOS has a default implementation, the watch does
|
|
/// not, because the gallery has to be populated.
|
|
///
|
|
/// One entry, with nothing selected: that means "whatever the app is set
|
|
/// to", which is the filter the user already chose there.
|
|
func recommendations() -> [AppIntentRecommendation<WatchCalendarSelectionIntent>] {
|
|
[AppIntentRecommendation(intent: WatchCalendarSelectionIntent(),
|
|
description: Text("Calendarr"))]
|
|
}
|
|
|
|
/// The app-level filter applies first; a per-complication selection narrows
|
|
/// further. An empty selection means "whatever the app is set to".
|
|
private func resolved(_ configuration: WatchCalendarSelectionIntent) -> CalendarrSnapshot? {
|
|
guard case .ok(let snapshot) = SnapshotStore().read() else { return nil }
|
|
return snapshot
|
|
.filtered(excludingCalendarKeys: WatchFilterStore.load())
|
|
.filtered(toCalendarKeys: Set((configuration.selectedCalendars ?? []).map(\.id)))
|
|
}
|
|
}
|