Uses the string form rather than .watchOS(.v26): that enum case requires
PackageDescription 6.2, and raising swift-tools-version for one constant
would change defaults across the whole manifest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signing out deletes the cached events — keeping them would leak the previous
user's calendar to anything reading the container — so an absent cache alone
cannot distinguish "signed out" from "Calendarr has never run on this machine".
Those need different messages, so read() now falls back to the session record
and reports .loggedOut when one exists and says so. Every consumer would
otherwise have to reimplement that two-step, and any that forgot would tell a
signed-out user to go install an app they already have.
SnapshotTheme only had the compiler's internal memberwise initializer, so no
consumer outside the package could actually build one.
Calendarr already publishes an event snapshot into its App Group container for
the widgets to render. That channel is what a second Mac app needs too, so this
turns an internal detail into a documented contract rather than something a
second app reverse-engineers and then drifts from.
Foundation only — no SwiftUI, no UIKit, no AppKit — so one source compiles for
iOS, Mac Catalyst and native macOS. Building against the native macOS SDK is the
point: it fails loudly on a UIKit leak that a Catalyst build would accept.
Three things earn their place here rather than being copied per app:
CalendarrAppGroup resolves the container identifier per platform. macOS requires
the Team ID prefix and iOS forbids it, and a mismatch fails silently — nil
container, every read and write a no-op, widgets blank forever.
CalendarKey normalises the two ways the server reports calendar_id (bare for
CalDAV, "local-3" style for everything else). Normalising them differently is
what made per-calendar filtering quietly do nothing for some sources.
The snapshot schema is versioned, with the coverage window now explicit. That
window is the easiest thing to get wrong downstream: outside it the snapshot
carries no information, which is not the same as no events, and rendering an
empty week for an uncovered date produces a screen that looks right and is
false. covers() exists so consumers can tell the two apart.
Reads return a named result rather than an optional. "Never ran", "signed out",
"too new to read" and "unreadable" need different messages, and a bare nil is
how the App Group misconfiguration used to hide.
Writing is behind @_spi(Writer) so a read-only consumer cannot corrupt the
shared cache. Writes post a Darwin notification, the one cross-process signal
that works between two sandboxed apps without an extra entitlement.
The six colour values stay flat on the wire instead of nesting under `theme`,
which would read better. A v1 reader requires them flat, so nesting would have
broken every older build on the first v2 write; `theme` is a computed view.
Tests pin both directions of compatibility, since the two apps ship
independently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>