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>
31 lines
1.4 KiB
Swift
31 lines
1.4 KiB
Swift
import Foundation
|
|
|
|
/// Stable identifier for one calendar, used to tie a snapshot event back to the
|
|
/// calendar it came from.
|
|
///
|
|
/// This exists because the server reports `calendar_id` inconsistently: a raw
|
|
/// numeric for CalDAV sources, but `"<source>-<id>"` for local, iCal, Google and
|
|
/// Home Assistant (e.g. `"local-3"`). Any consumer that filters by calendar has
|
|
/// to normalise the two forms identically, or filtering silently does nothing
|
|
/// for the prefixed sources — which is exactly the bug this rule was written to
|
|
/// fix. Keeping the rule in one shared place is why it lives here rather than
|
|
/// being reimplemented per app.
|
|
public enum CalendarKey {
|
|
/// Build a key, stripping a redundant `"<source>-"` prefix from the id.
|
|
public static func make(source: String, calendarId: String) -> String {
|
|
var id = calendarId
|
|
let prefix = "\(source)-"
|
|
if id.hasPrefix(prefix) { id = String(id.dropFirst(prefix.count)) }
|
|
return "\(source):\(id)"
|
|
}
|
|
|
|
/// Split a key back into its source and numeric id.
|
|
/// Returns nil for malformed keys or non-numeric ids.
|
|
public static func parse(_ key: String) -> (source: String, id: Int)? {
|
|
guard let colon = key.firstIndex(of: ":") else { return nil }
|
|
let source = String(key[..<colon])
|
|
guard let id = Int(key[key.index(after: colon)...]) else { return nil }
|
|
return (source, id)
|
|
}
|
|
}
|