Repository Structure
The app is split across two repositories: a product repo (RoamerMaker) and a shared packages repo (RoamerFoundation). Foundation packages own the generic CloudKit and StoreKit engines; the app repo owns feature gating, pricing tiers, and UX policy.
Packages are referenced as local Swift packages via relative path. A ci_post_clone.sh script clones RoamerFoundation alongside RoamerMaker on Xcode Cloud so path references resolve identically in CI and locally — no package registry, no SPM mirroring, no build configuration divergence.
Apple Frameworks in Use
The app entry point is RoamerMakerApp, which composes either IPadRootView or IPhoneRootView through AdaptiveRootView. Navigation is state-driven via NavigationModel (NavigationPath + typed NavigationDestination) rather than view-local routing. Shared stores are injected once at the root via environmentObject so sync pipelines are never duplicated across hierarchies.
RoamerMaker uses CloudKit public records (AppConfig, IAPPromo) for remotely managed app policy and merchandising, and CloudKit private records for user-owned inventory data. Model types conform to CloudKitRecord and persist recordChangeTag and modificationDate so conflicts can be merged with context. Cloud traffic flows through CloudKitFetcher<T> and is reconciled with zone change tokens via CKFetchRecordZoneChangesOperation to avoid full-refetch loops.
RoamerStoreKit.StoreKitController listens to Transaction.updates and optionally PurchaseIntent.intents, while RoamerMakerStoreKitController wires those events into app entitlements. The app computes access from Product.SubscriptionInfo.status(for:) plus verified Transaction.currentEntitlements, mapping product IDs to SubscriptionTier and Feature. EnvironmentDetector reads AppTransaction.shared so sandbox vs. production behaviour is derived from the signed transaction, not build configuration alone.
Foundation underlies all persistence and serialisation: JSON caches for promos, products, and dismissal windows; UserDefaults-backed change tokens; and app-group entitlement snapshots. Actor services (PromoService, AppConfigService) use Foundation types for deterministic cache mutation and time-window evaluation. A non-obvious path: CKAsset file URLs are copied into app-managed storage because CloudKit temporary asset URLs are not durable past the fetch operation.
UI-reactive state is implemented with ObservableObject and @Published across stores and controllers. RoamerMakerStoreKitController forwards RoamerStoreKit publisher output with assign(to:) to avoid duplicate buffering logic. Concurrency-heavy work stays in async/actor context; Combine is a view-update transport only.
UIKit is introduced only where SwiftUI is not a direct fit for the required behaviour. AppVersionManager presents force-update and soft-update alerts with UIAlertController and opens App Store targets with UIApplication.shared.open. DebugLogsView bridges UIActivityViewController for exporting diagnostics. These edges are isolated and do not touch core state or services.
CoreLocation is represented in the app target as authorization state (AppState.LocationAuthorizationStatus) rather than direct manager orchestration in views. The reusable manager implementation lives in RoamerFoundation/RoamerLocation, keeping the app focused on product behaviour and location policy portable across all Roamer apps without coupling UI flows to framework plumbing.
DebugLogger writes each event to both OSLog.Logger and a rotating on-disk log file for field debugging. Global helper functions (logDebug, logCloudKit, logError) dispatch onto the logger actor so call sites stay simple while writes stay serialised. This dual-path design provides live console visibility and exportable postmortem traces from user devices.
Sync Architecture
Optimistic cache write with a dual-cache model is the foundation of RoamerMaker's UX and data correctness. It removes network latency from every user action, prevents UI rollback during save-reconcile races, and enables resilient offline behaviour — while still converging to CloudKit truth.
User edit applied immediately to the in-memory published store — UI updates without any network wait.
Same record written to local durable cache synchronously, before the network call begins.
CloudKit save runs asynchronously in the background, never blocking interaction.
Pending markers (pendingEditIDs, pendingSyncIDs) protect the record from stale server overwrites during the save window.
UserDefaults-backed typed caches in the CloudKit package, plus change-token persistence and fail-open stale reads. Guarantees continuity across launches and offline periods.
@MainActor ObservableObject arrays and properties that drive SwiftUI rendering directly. Guarantees immediate interaction feedback with zero display latency.
Background reconciliation fetches server deltas using zone change tokens — no full reloads.
Merge strategy is server-preferred by default, except for records currently marked as locally pending.
Once a successful reconcile cycle confirms the latest state, pending protection clears and normal server authority resumes.
- Removes network latency from core workflows — every interaction is instant.
- Prevents UI rollback and flicker during save-reconcile races.
- Enables resilient offline and degraded-network behaviour while converging to CloudKit truth.
- The foundation that makes every major user flow feel fast and trustworthy.
Design Principles
Shared engines, app-specific rules
RoamerFoundation packages own the generic CloudKit and StoreKit mechanics; RoamerMaker owns feature gating, pricing tiers, and UX policy. This keeps package APIs reusable across the Roamer product line while letting each app evolve its product model independently.
Cache-first UX, reconcile in the background
Writes are applied to local state immediately; server sync follows asynchronously. The dual-cache model — durable on-disk and in-memory published — ensures both launch continuity and instant interaction feedback. See Sync Architecture above for the full write-path and reconciliation detail.
Actor isolation for side-effectful services
Promo, config, and logging services are Swift actor types so shared mutable state is serialised without manual locking. UI stores remain @MainActor, keeping mutation and rendering deterministic and easier to reason about.
Fail-open for network and iCloud variance
If remote config or promo fetches fail, startup still proceeds with cached or safe defaults. Account-switch handling explicitly clears caches and sync tokens before reload, preventing cross-account data bleed under real-world iCloud variability.
Use UIKit only at the seams
SwiftUI is the default; UIKit is introduced only for behaviours that are operationally better served there — update alerts, share sheets, responder-level keyboard interactions. This keeps most of the codebase declarative without sacrificing practical platform integration.