Add StadiumAlias CloudKit sync and offline-first data architecture
- Add CKStadiumAlias model for CloudKit record mapping - Add fetchStadiumAliases/fetchStadiumAliasChanges to CloudKitService - Add syncStadiumAliases to CanonicalSyncService for delta sync - Add subscribeToStadiumAliasUpdates for push notifications - Update cloudkit_import.py with --stadium-aliases-only option Data Architecture Updates: - Remove obsolete provider files (CanonicalDataProvider, CloudKitDataProvider, StubDataProvider) - AppDataProvider now reads exclusively from SwiftData - Add background CloudKit sync on app startup (non-blocking) - Document data architecture in CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
72
CLAUDE.md
72
CLAUDE.md
@@ -50,11 +50,75 @@ This is an iOS app for planning multi-stop sports road trips. It uses **Clean MV
|
||||
- `Services/POISearchService` - Finds nearby restaurants, attractions via MKLocalSearch
|
||||
- `Services/PDFAssetPrefetcher` - Parallel prefetching of all PDF assets
|
||||
|
||||
### Data Storage Strategy
|
||||
### Data Architecture (Offline-First)
|
||||
|
||||
- **CloudKit Public DB**: Read-only schedules, stadiums, teams (shared across all users)
|
||||
- **SwiftData Local**: User's saved trips, preferences, cached schedules
|
||||
- **No network dependency** for trip planning once schedules are synced
|
||||
**CRITICAL: `AppDataProvider.shared` is the ONLY source of truth for canonical data.**
|
||||
|
||||
All code that reads stadiums, teams, games, or league structure MUST use `AppDataProvider.shared`. Never access CloudKit or SwiftData directly for this data.
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ Bundled JSON │ │ SwiftData │ │ CloudKit │
|
||||
│ (App Bundle) │ │ (Local Store) │ │ (Remote Sync) │
|
||||
└────────┬─────────┘ └────────▲─────────┘ └────────┬─────────┘
|
||||
│ │ │
|
||||
│ Bootstrap │ Read │ Background
|
||||
│ (first launch) │ │ Sync
|
||||
▼ │ ▼
|
||||
┌─────────────────────────────────┴────────────────────────────────────┐
|
||||
│ AppDataProvider.shared │
|
||||
│ (Single Source of Truth) │
|
||||
└─────────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
All Features, ViewModels, Services
|
||||
```
|
||||
|
||||
**App Startup Flow:**
|
||||
1. **Bootstrap** (first launch only): `BootstrapService` loads bundled JSON → SwiftData
|
||||
2. **Configure**: `AppDataProvider.shared.configure(with: context)`
|
||||
3. **Load**: `AppDataProvider.shared.loadInitialData()` reads SwiftData into memory
|
||||
4. **App usable immediately** with local data
|
||||
5. **Background sync**: `CanonicalSyncService.syncAll()` fetches CloudKit → updates SwiftData (non-blocking)
|
||||
6. **Reload**: After sync completes, `loadInitialData()` refreshes in-memory cache
|
||||
|
||||
**Offline Handling:**
|
||||
- SwiftData always has data (from bootstrap or last successful CloudKit sync)
|
||||
- If CloudKit unavailable, app continues with existing local data
|
||||
- First launch + offline = bootstrap data used
|
||||
|
||||
**Canonical Data Models** (in SwiftData, synced from CloudKit):
|
||||
- `CanonicalStadium` → `Stadium` (domain)
|
||||
- `CanonicalTeam` → `Team` (domain)
|
||||
- `CanonicalGame` → `Game` (domain)
|
||||
- `LeagueStructureModel`
|
||||
- `TeamAlias`, `StadiumAlias`
|
||||
|
||||
**User Data Models** (local only, not synced):
|
||||
- `SavedTrip`, `StadiumVisit`, `UserPreferences`, `Achievement`
|
||||
|
||||
**Correct Usage:**
|
||||
```swift
|
||||
// ✅ CORRECT - Use AppDataProvider
|
||||
let stadiums = AppDataProvider.shared.stadiums
|
||||
let teams = AppDataProvider.shared.teams
|
||||
let games = try await AppDataProvider.shared.fetchGames(sports: sports, startDate: start, endDate: end)
|
||||
let richGames = try await AppDataProvider.shared.fetchRichGames(...)
|
||||
|
||||
// ❌ WRONG - Never access CloudKit directly for reads
|
||||
let stadiums = try await CloudKitService.shared.fetchStadiums() // NO!
|
||||
|
||||
// ❌ WRONG - Never fetch canonical data from SwiftData directly
|
||||
let descriptor = FetchDescriptor<CanonicalStadium>()
|
||||
let stadiums = try context.fetch(descriptor) // NO! (except in DataProvider/Sync/Bootstrap)
|
||||
```
|
||||
|
||||
**Allowed Direct SwiftData Access:**
|
||||
- `DataProvider.swift` - It IS the data provider
|
||||
- `CanonicalSyncService.swift` - CloudKit → SwiftData sync
|
||||
- `BootstrapService.swift` - Initial data population
|
||||
- `StadiumIdentityService.swift` - Specialized identity resolution for stadium renames
|
||||
- User data (e.g., `StadiumVisit`) - Not canonical data
|
||||
|
||||
### Key Data Flow
|
||||
|
||||
|
||||
Reference in New Issue
Block a user