diff --git a/CalendarrKit/.gitignore b/CalendarrKit/.gitignore new file mode 100644 index 0000000..000dfed --- /dev/null +++ b/CalendarrKit/.gitignore @@ -0,0 +1,5 @@ +.build/ +.swiftpm/ +DerivedData/ +*.xcuserstate +.DS_Store diff --git a/CalendarrKit/Package.swift b/CalendarrKit/Package.swift new file mode 100644 index 0000000..84ebb17 --- /dev/null +++ b/CalendarrKit/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "CalendarrKit", + platforms: [ + .iOS(.v17), + // String form, not `.v26`: that enum case needs PackageDescription 6.2, + // and raising swift-tools-version for one constant would change + // defaults across the whole manifest. + .watchOS("26.0"), + .macOS(.v14), + .macCatalyst(.v17), + ], + products: [ + // Foundation only — deliberately no SwiftUI, no UIKit, no AppKit, so it + // compiles for iOS, Mac Catalyst and native macOS from one source. + .library(name: "CalendarrCore", targets: ["CalendarrCore"]), + ], + targets: [ + .target( + name: "CalendarrCore", + // Matches the apps, which build in Swift 5 language mode. Note that + // this package deliberately does NOT set defaultIsolation(MainActor), + // even though the Calendarr iOS app target does: the widget extension + // and the read-only consumer app have no MainActor default, and making + // the package MainActor-by-default would force `await` at every + // SnapshotStore.read() call site. + swiftSettings: [.swiftLanguageMode(.v5)] + ), + .testTarget( + name: "CalendarrCoreTests", + dependencies: ["CalendarrCore"], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + ] +) diff --git a/CalendarrKit/README.md b/CalendarrKit/README.md new file mode 100644 index 0000000..e206dd2 --- /dev/null +++ b/CalendarrKit/README.md @@ -0,0 +1,105 @@ +# CalendarrKit + +Shared code between Calendarr (iOS / Mac Catalyst) and other apps from the same +developer team that want to **read** the user's calendar. + +`CalendarrCore` depends on Foundation only — no SwiftUI, no UIKit, no AppKit — so +it compiles for iOS, Mac Catalyst and native macOS from one source. That +constraint is load-bearing: `swift build` against the native macOS SDK is what +catches a UIKit import before it reaches a consumer, because a Mac Catalyst build +would compile it happily. + +## What a reading app gets + +```swift +import CalendarrCore + +let store = SnapshotStore() // uses CalendarrAppGroup.current + +switch store.read() { +case .ok(let snap): + snap.events // [SnapshotEvent] + snap.theme // the colours Calendarr is currently rendering with + snap.writtenAt // "as of 14:32" + snap.coverageStart // see "Coverage" below + snap.coverageEnd +case .neverWritten: // Calendarr has never run here → "open Calendarr once" +case .loggedOut: // signed out → "sign in to Calendarr", discard old events +case .incompatible: // Calendarr is newer than this package → "update the app" +case .unreadable(let why): // almost always a wrong App Group; log `why` +} + +store.readCalendars() // [SnapshotCalendar] for a filter UI +``` + +React to changes without polling: + +```swift +let token = SnapshotChangeNotifier.observe { /* hop to your actor, read again */ } +// keep `token` alive; releasing it removes the observer +``` + +Also reload on window activation — the notification is a best-effort signal, and +missing one should leave you slightly stale, never wrong. + +## Coverage — the part that bites + +The snapshot is not the whole calendar. Outside `coverageStart..