Skip to content

Add iPad and Mac Catalyst surfaces - #162

Open
kyleve wants to merge 3 commits into
codex/multi-device-recordingfrom
codex/ipad-mac-where
Open

Add iPad and Mac Catalyst surfaces#162
kyleve wants to merge 3 commits into
codex/multi-device-recordingfrom
codex/ipad-mac-where

Conversation

@kyleve

@kyleve kyleve commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • add an adaptive two-column iPad and Mac Catalyst shell while preserving the iPhone tab experience and per-section navigation state
  • introduce platform-aware recording participation: new iPads opt in, upgraded iPads preserve their prior behavior, and Catalyst remains management-only
  • add an explicit “Join Existing iCloud Data” path without seeding data or enabling local recording
  • add a shared App Group presentation contract powering a Mac summary widget and an opt-in native menu-bar login item, and enable the share extension on Catalyst
  • make external SwiftData and CloudKit writes refresh glance surfaces reliably through history classification, coalesced publication, and a publisher shared with App Intents
  • add Catalyst packaging, entitlements, Hardened Runtime, CI verification, architecture rules, documentation, and regression coverage

Why

PR #160 establishes synchronized per-device recording controls. This follow-up uses that model to make iPad a first-class viewer and manager and to add a Mac Catalyst management experience without turning the Mac into another location-recording device.

User impact

  • iPhone remains tab-based and recording-enabled by default.
  • New iPad installs begin with automatic recording off; upgraded installs retain their existing policy.
  • Mac can browse and manage Where data, use the share extension, view today’s regions and year-to-date day counts, and optionally enable a native menu-bar glance.
  • “Join Existing iCloud Data” opens the real synced store without demo seeding or local recording.

Reliability

  • local writers are stamped so SwiftData persistent history can distinguish local saves from external CloudKit and share-extension imports
  • widget publication coalesces concurrent requests, retries failed writes, and shares one publisher between the app and App Intents
  • the menu-bar helper reads an atomic presentation artifact from the App Group and preserves last-good data with freshness and error state
  • Mac executables use Hardened Runtime, and CI verifies that the helper, widget, and share extension are embedded with valid helper metadata

Validation

  • ./test --all --no-generate — 1,587 tests passed
  • changed snapshot suites — 3/3 tests passed; every capture matched byte-for-byte
  • unsigned Where-Catalyst build — passed, including embedded-binary validation for the helper, widget, and share extension
  • swift run bumper config .
  • swift run bumper test . — 16 tests passed
  • swift run bumper lint . --timings
  • ./swiftformat --lint
  • ./xcstrings --lint
  • ./attribution --check
  • workflow YAML parse and git diff --check

A signed installed-app smoke test is still valuable for App Group access, CloudKit imports, SMAppService registration, widget discovery, and the where:// launch path.

Stack

Depends on #160.

kyleve commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Architecture note: keep WhereSurface as the published read model

I considered replacing the shared artifact with direct store access from the widget and native menu-bar processes. The conclusion is to keep the current boundary:

  • The Catalyst app already uses the real WhereServices / SwiftData store.
  • Widgets consume the broader WidgetSnapshot (all relevant day regions, totals, and appearances), so their near-term flexibility is not limited by the narrower WhereSurfaceSnapshot.
  • WhereSurface primarily lets the long-lived login helper remain a small, Foundation-only glance process that is independent of SwiftData schema loading, migrations, onboarding/scope ownership, RegionKit, and CloudKit.
  • Direct shared-store access is technically possible, but every secondary process would need one injected, read-only .localOnly container. The app must remain the only CloudKit synchronizer: Apple explicitly warns that an app and extension must not load the same store through multiple CloudKit-synchronizing containers, even across processes (TN3164).
  • For widgets, the published snapshot keeps timeline work bounded and follows Apple's recommendation that the containing app prepare shared widget data in advance (WidgetKit strategy).

If the menu-bar item later becomes an interactive history browser, a hybrid is worth revisiting: keep widgets snapshot-based, and give the helper a narrow read-only store API. For the current glance UI, the small additive wire contract buys more lifecycle and compatibility safety than direct access buys flexibility.

One follow-up did fall out of this review: atomic replacement prevents partial JSON, but Apple requires file coordination for custom files accessed across App Group processes. This PR will therefore coordinate both the shared JSON writer and readers with NSFileCoordinator (shared-file guidance).

@kyleve
kyleve marked this pull request as ready for review August 2, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant