feat(sync): add CloudKit sync for dynamic sports

- Add CKSport model to parse CloudKit Sport records
- Add fetchSportsForSync() to CloudKitService for delta fetching
- Add syncSports() and mergeSport() to CanonicalSyncService
- Update DataProvider with dynamicSports support and allSports computed property
- Update MockAppDataProvider with matching dynamic sports support
- Add comprehensive documentation for adding new sports

The app can now sync sport definitions from CloudKit, enabling new sports
to be added without app updates. Sports are fetched, merged into SwiftData,
and exposed via AppDataProvider.allSports alongside built-in Sport enum cases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 18:27:56 -06:00
parent dc278085de
commit f180e5bfed
10 changed files with 2080 additions and 161 deletions

View File

@@ -18,6 +18,7 @@ final class MockAppDataProvider: ObservableObject {
@Published private(set) var teams: [Team] = []
@Published private(set) var stadiums: [Stadium] = []
@Published private(set) var dynamicSports: [DynamicSport] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
@Published private(set) var errorMessage: String?
@@ -26,6 +27,7 @@ final class MockAppDataProvider: ObservableObject {
private var teamsById: [String: Team] = [:]
private var stadiumsById: [String: Stadium] = [:]
private var dynamicSportsById: [String: DynamicSport] = [:]
private var games: [Game] = []
private var gamesById: [String: Game] = [:]
@@ -80,12 +82,19 @@ final class MockAppDataProvider: ObservableObject {
self.gamesById = Dictionary(uniqueKeysWithValues: newGames.map { ($0.id, $0) })
}
func setDynamicSports(_ newSports: [DynamicSport]) {
self.dynamicSports = newSports
self.dynamicSportsById = Dictionary(uniqueKeysWithValues: newSports.map { ($0.id, $0) })
}
func reset() {
teams = []
stadiums = []
dynamicSports = []
games = []
teamsById = [:]
stadiumsById = [:]
dynamicSportsById = [:]
gamesById = [:]
isLoading = false
error = nil
@@ -156,6 +165,17 @@ final class MockAppDataProvider: ObservableObject {
teams.filter { $0.sport == sport }
}
func dynamicSport(for id: String) -> DynamicSport? {
dynamicSportsById[id]
}
/// All sports: built-in Sport enum cases + CloudKit-defined DynamicSports
var allSports: [any AnySport] {
let builtIn: [any AnySport] = Sport.allCases
let dynamic: [any AnySport] = dynamicSports
return builtIn + dynamic
}
// MARK: - Game Filtering (Local Queries)
func filterGames(sports: Set<Sport>, startDate: Date, endDate: Date) async throws -> [Game] {
@@ -287,4 +307,13 @@ extension MockAppDataProvider {
/// Get stadiums count
var stadiumsCount: Int { stadiums.count }
/// Add a single dynamic sport
func addDynamicSport(_ sport: DynamicSport) {
dynamicSports.append(sport)
dynamicSportsById[sport.id] = sport
}
/// Get dynamic sports count
var dynamicSportsCount: Int { dynamicSports.count }
}