refactor: change domain model IDs from UUID to String canonical IDs
This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,14 +21,9 @@ final class AppDataProvider: ObservableObject {
|
||||
@Published private(set) var error: Error?
|
||||
@Published private(set) var errorMessage: String?
|
||||
|
||||
private var teamsById: [UUID: Team] = [:]
|
||||
private var stadiumsById: [UUID: Stadium] = [:]
|
||||
private var stadiumsByCanonicalId: [String: Stadium] = [:]
|
||||
private var teamsByCanonicalId: [String: Team] = [:]
|
||||
|
||||
// Canonical ID lookups for game conversion
|
||||
private var canonicalTeamUUIDs: [String: UUID] = [:]
|
||||
private var canonicalStadiumUUIDs: [String: UUID] = [:]
|
||||
// Lookup dictionaries - keyed by canonical ID (String)
|
||||
private var teamsById: [String: Team] = [:]
|
||||
private var stadiumsById: [String: Stadium] = [:]
|
||||
|
||||
private var modelContext: ModelContext?
|
||||
|
||||
@@ -63,11 +58,11 @@ final class AppDataProvider: ObservableObject {
|
||||
|
||||
// Convert to domain models and build lookups
|
||||
var loadedStadiums: [Stadium] = []
|
||||
var stadiumLookup: [String: Stadium] = [:]
|
||||
for canonical in canonicalStadiums {
|
||||
let stadium = canonical.toDomain()
|
||||
loadedStadiums.append(stadium)
|
||||
stadiumsByCanonicalId[canonical.canonicalId] = stadium
|
||||
canonicalStadiumUUIDs[canonical.canonicalId] = stadium.id
|
||||
stadiumLookup[stadium.id] = stadium
|
||||
}
|
||||
|
||||
// Fetch canonical teams from SwiftData
|
||||
@@ -78,31 +73,17 @@ final class AppDataProvider: ObservableObject {
|
||||
|
||||
// Convert to domain models
|
||||
var loadedTeams: [Team] = []
|
||||
var teamLookup: [String: Team] = [:]
|
||||
for canonical in canonicalTeams {
|
||||
// Get stadium UUID for this team
|
||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
||||
let team = canonical.toDomain(stadiumUUID: stadiumUUID)
|
||||
let team = canonical.toDomain()
|
||||
loadedTeams.append(team)
|
||||
teamsByCanonicalId[canonical.canonicalId] = team
|
||||
canonicalTeamUUIDs[canonical.canonicalId] = team.id
|
||||
teamLookup[team.id] = team
|
||||
}
|
||||
|
||||
self.teams = loadedTeams
|
||||
self.stadiums = loadedStadiums
|
||||
|
||||
// Build lookup dictionaries (use reduce to handle potential duplicates gracefully)
|
||||
self.teamsById = loadedTeams.reduce(into: [:]) { dict, team in
|
||||
if dict[team.id] != nil {
|
||||
print("⚠️ Duplicate team UUID: \(team.id) - \(team.name)")
|
||||
}
|
||||
dict[team.id] = team
|
||||
}
|
||||
self.stadiumsById = loadedStadiums.reduce(into: [:]) { dict, stadium in
|
||||
if dict[stadium.id] != nil {
|
||||
print("⚠️ Duplicate stadium UUID: \(stadium.id) - \(stadium.name)")
|
||||
}
|
||||
dict[stadium.id] = stadium
|
||||
}
|
||||
self.teamsById = teamLookup
|
||||
self.stadiumsById = stadiumLookup
|
||||
|
||||
} catch {
|
||||
self.error = error
|
||||
@@ -123,11 +104,11 @@ final class AppDataProvider: ObservableObject {
|
||||
|
||||
// MARK: - Data Access
|
||||
|
||||
func team(for id: UUID) -> Team? {
|
||||
func team(for id: String) -> Team? {
|
||||
teamsById[id]
|
||||
}
|
||||
|
||||
func stadium(for id: UUID) -> Stadium? {
|
||||
func stadium(for id: String) -> Stadium? {
|
||||
stadiumsById[id]
|
||||
}
|
||||
|
||||
@@ -156,47 +137,27 @@ final class AppDataProvider: ObservableObject {
|
||||
let canonicalGames = try context.fetch(descriptor)
|
||||
|
||||
// Filter by sport and convert to domain models
|
||||
let result = canonicalGames.compactMap { canonical -> Game? in
|
||||
return canonicalGames.compactMap { canonical -> Game? in
|
||||
guard sportStrings.contains(canonical.sport) else { return nil }
|
||||
|
||||
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
|
||||
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
|
||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
||||
|
||||
return canonical.toDomain(
|
||||
homeTeamUUID: homeTeamUUID,
|
||||
awayTeamUUID: awayTeamUUID,
|
||||
stadiumUUID: stadiumUUID
|
||||
)
|
||||
return canonical.toDomain()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Fetch a single game by ID
|
||||
func fetchGame(by id: UUID) async throws -> Game? {
|
||||
/// Fetch a single game by canonical ID
|
||||
func fetchGame(by id: String) async throws -> Game? {
|
||||
guard let context = modelContext else {
|
||||
throw DataProviderError.contextNotConfigured
|
||||
}
|
||||
|
||||
let idString = id.uuidString
|
||||
let descriptor = FetchDescriptor<CanonicalGame>(
|
||||
predicate: #Predicate<CanonicalGame> { $0.canonicalId == idString }
|
||||
predicate: #Predicate<CanonicalGame> { $0.canonicalId == id }
|
||||
)
|
||||
|
||||
guard let canonical = try context.fetch(descriptor).first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
|
||||
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
|
||||
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
|
||||
|
||||
return canonical.toDomain(
|
||||
homeTeamUUID: homeTeamUUID,
|
||||
awayTeamUUID: awayTeamUUID,
|
||||
stadiumUUID: stadiumUUID
|
||||
)
|
||||
return canonical.toDomain()
|
||||
}
|
||||
|
||||
/// Fetch games with full team and stadium data
|
||||
|
||||
Reference in New Issue
Block a user