Addresses issue where Houston Astros only shows 7 games in "By Games" mode. Documents plan to remove arbitrary date restrictions and implement proper delta sync using CloudKit modificationDate. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.0 KiB
Delta Sync & Game Browsing Design
Date: 2026-01-12 Status: Approved Problem: Houston Astros only shows 7 games in "By Games" mode
Problem Analysis
Root Causes
-
90-day browsing window in
TripCreationViewModel.loadGamesForBrowsing()arbitrarily limits games shown in "By Games" mode -
6-month CloudKit sync window in
CanonicalSyncService.syncGames()only syncs games within 6 months of today -
Wrong query field - CloudKit queries filter by game
dateTime(when game is scheduled) instead ofmodificationDate(when record was last modified)
Impact
- Users cannot plan trips around games outside the 90-day window
- New schedules added to CloudKit may not sync if games fall outside the window
- Delta sync doesn't work correctly - modified historical games never sync
Solution
1. Unified Delta Sync Pattern
All CloudKit data types follow the same pattern:
func fetchForSync(since lastSync: Date?) async throws -> [T] {
let predicate: NSPredicate
if let lastSync = lastSync {
// Delta: only modified records
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
} else {
// First sync: ALL records
predicate = NSPredicate(value: true)
}
// ... execute query
}
First sync (lastSync == nil): Fetch all records from CloudKit. This handles the edge case where CloudKit is updated after app build but before user installs.
Delta sync (lastSync has value): Fetch only records with modificationDate >= lastSync.
2. CloudKitService Changes
| Method | Before | After |
|---|---|---|
fetchStadiumsForSync |
() |
(since: Date?) |
fetchTeamsForSync |
(for: Sport) |
(since: Date?) |
fetchGamesForSync |
(sports:, startDate:, endDate:) |
(since: Date?) |
fetchLeagueStructureChanges |
(since: Date?) |
No change |
fetchTeamAliasChanges |
(since: Date?) |
No change |
fetchStadiumAliasChanges |
(since: Date?) |
No change |
Teams optimization: Fetch all teams in one call instead of 7 separate per-sport calls.
3. CanonicalSyncService Changes
Remove date range calculations from syncGames():
// BEFORE
let startDate = lastSync ?? Date()
let endDate = Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date()
let syncGames = try await cloudKitService.fetchGamesForSync(
sports: Set(Sport.allCases),
startDate: startDate,
endDate: endDate
)
// AFTER
let syncGames = try await cloudKitService.fetchGamesForSync(since: lastSync)
4. DataProvider Naming Convention
Rename methods to clarify local vs network operations:
| Before | After | Purpose |
|---|---|---|
fetchGames(sports:, startDate:, endDate:) |
filterGames(sports:, startDate:, endDate:) |
Local query with date filter |
fetchRichGames(...) |
filterRichGames(...) |
Local query with date filter |
| New | allGames(for sports:) |
Local query, no date filter |
| New | allRichGames(for sports:) |
Local query, no date filter |
Convention: filter* = local query with constraints, all* = local query without date filter.
5. TripCreationViewModel Changes
Remove 90-day browsing limit:
// BEFORE
let browseEndDate = Calendar.current.date(byAdding: .day, value: 90, to: Date()) ?? endDate
games = try await dataProvider.fetchGames(
sports: selectedSports,
startDate: Date(),
endDate: browseEndDate
)
// AFTER
games = try await dataProvider.allGames(for: selectedSports)
Files to Modify
| File | Changes |
|---|---|
SportsTime/Core/Services/CloudKitService.swift |
Update fetch signatures, query by modificationDate |
SportsTime/Core/Services/CanonicalSyncService.swift |
Remove date range logic, pass lastSync directly |
SportsTime/Core/Services/DataProvider.swift |
Rename methods, add allGames/allRichGames |
SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift |
Use allGames instead of date-filtered fetch |
SportsTimeTests/Mocks/MockCloudKitService.swift |
Update mock signatures |
SportsTimeTests/Mocks/MockAppDataProvider.swift |
Update mock signatures, add new methods |
Edge Cases
App built before CloudKit update
| Date | Event |
|---|---|
| 1/12 | App published with bundled JSON |
| 1/13 | Developer adds NWSL 2026 to CloudKit |
| 1/14 | User installs app |
Handled by: First sync (lastSync == nil) fetches ALL records from CloudKit, including the 1/13 additions.
New league added
When a new league (e.g., NWSL) is added to CloudKit:
- New records have
modificationDate= creation timestamp - Delta sync picks them up if
modificationDate >= lastSync - Works for games, teams, stadiums, and all other data types
Testing
Existing tests should pass with method renames. Key scenarios to verify:
- First launch: Full sync fetches all data
- Subsequent launch: Delta sync only fetches modified records
- "By Games" mode shows all available games
- New CloudKit data syncs correctly after app update