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:
@@ -174,12 +174,14 @@ final class PhotoImportViewModel {
|
||||
|
||||
// Create the visit
|
||||
let visit = StadiumVisit(
|
||||
canonicalStadiumId: match.stadium.id.uuidString,
|
||||
stadiumUUID: match.stadium.id,
|
||||
stadiumId: match.stadium.id,
|
||||
stadiumNameAtVisit: match.stadium.name,
|
||||
visitDate: match.game.dateTime,
|
||||
sport: match.game.sport,
|
||||
visitType: .game,
|
||||
gameId: match.game.id,
|
||||
homeTeamId: match.homeTeam.id,
|
||||
awayTeamId: match.awayTeam.id,
|
||||
homeTeamName: match.homeTeam.fullName,
|
||||
awayTeamName: match.awayTeam.fullName,
|
||||
finalScore: match.formattedFinalScore,
|
||||
|
||||
@@ -41,10 +41,10 @@ final class ProgressViewModel {
|
||||
let visitedStadiumIds = Set(
|
||||
visits
|
||||
.filter { $0.sportEnum == selectedSport }
|
||||
.compactMap { visit -> UUID? in
|
||||
.compactMap { visit -> String? in
|
||||
// Match visit's canonical stadium ID to a stadium
|
||||
stadiums.first { stadium in
|
||||
stadium.id == visit.stadiumUUID
|
||||
stadium.id == visit.stadiumId
|
||||
}?.id
|
||||
}
|
||||
)
|
||||
@@ -62,11 +62,11 @@ final class ProgressViewModel {
|
||||
}
|
||||
|
||||
/// Stadium visit status indexed by stadium ID
|
||||
var stadiumVisitStatus: [UUID: StadiumVisitStatus] {
|
||||
var statusMap: [UUID: StadiumVisitStatus] = [:]
|
||||
var stadiumVisitStatus: [String: StadiumVisitStatus] {
|
||||
var statusMap: [String: StadiumVisitStatus] = [:]
|
||||
|
||||
// Group visits by stadium
|
||||
let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumUUID }
|
||||
let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumId }
|
||||
|
||||
for stadium in stadiums {
|
||||
if let stadiumVisits = visitsByStadium[stadium.id], !stadiumVisits.isEmpty {
|
||||
@@ -114,7 +114,7 @@ final class ProgressViewModel {
|
||||
.sorted { $0.visitDate > $1.visitDate }
|
||||
.prefix(10)
|
||||
.compactMap { visit -> VisitSummary? in
|
||||
guard let stadium = stadiums.first(where: { $0.id == visit.stadiumUUID }),
|
||||
guard let stadium = stadiums.first(where: { $0.id == visit.stadiumId }),
|
||||
let sport = visit.sportEnum else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import MapKit
|
||||
|
||||
struct ProgressMapView: View {
|
||||
let stadiums: [Stadium]
|
||||
let visitStatus: [UUID: StadiumVisitStatus]
|
||||
let visitStatus: [String: StadiumVisitStatus]
|
||||
@Binding var selectedStadium: Stadium?
|
||||
|
||||
// Fixed region for continental US - map is locked to this view
|
||||
|
||||
@@ -389,8 +389,7 @@ struct StadiumVisitSheet: View {
|
||||
|
||||
// Create the visit
|
||||
let visit = StadiumVisit(
|
||||
canonicalStadiumId: stadium.id.uuidString, // Simplified - in production use StadiumIdentityService
|
||||
stadiumUUID: stadium.id,
|
||||
stadiumId: stadium.id,
|
||||
stadiumNameAtVisit: stadium.name,
|
||||
visitDate: visitDate,
|
||||
sport: selectedSport,
|
||||
|
||||
@@ -523,7 +523,7 @@ extension VisitSource {
|
||||
|
||||
#Preview {
|
||||
let stadium = Stadium(
|
||||
id: UUID(),
|
||||
id: "stadium_preview_oracle_park",
|
||||
name: "Oracle Park",
|
||||
city: "San Francisco",
|
||||
state: "CA",
|
||||
|
||||
@@ -108,7 +108,7 @@ final class TripCreationViewModel {
|
||||
}
|
||||
|
||||
// Games
|
||||
var mustSeeGameIds: Set<UUID> = []
|
||||
var mustSeeGameIds: Set<String> = []
|
||||
var availableGames: [RichGame] = []
|
||||
var isLoadingGames: Bool = false
|
||||
|
||||
@@ -134,7 +134,7 @@ final class TripCreationViewModel {
|
||||
var selectedRegions: Set<Region> = [.east, .central, .west]
|
||||
|
||||
// Follow Team Mode
|
||||
var followTeamId: UUID?
|
||||
var followTeamId: String?
|
||||
var useHomeLocation: Bool = true
|
||||
|
||||
// Game First Mode - Trip duration for sliding windows
|
||||
@@ -148,8 +148,8 @@ final class TripCreationViewModel {
|
||||
|
||||
// MARK: - Cached Data
|
||||
|
||||
private var teams: [UUID: Team] = [:]
|
||||
private var stadiums: [UUID: Stadium] = [:]
|
||||
private var teams: [String: Team] = [:]
|
||||
private var stadiums: [String: Stadium] = [:]
|
||||
private var games: [Game] = []
|
||||
private(set) var currentPreferences: TripPreferences?
|
||||
|
||||
@@ -454,7 +454,7 @@ final class TripCreationViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func toggleMustSeeGame(_ gameId: UUID) {
|
||||
func toggleMustSeeGame(_ gameId: String) {
|
||||
if mustSeeGameIds.contains(gameId) {
|
||||
mustSeeGameIds.remove(gameId)
|
||||
} else {
|
||||
|
||||
@@ -13,13 +13,13 @@ import SwiftUI
|
||||
/// Renders a single timeline item (stop, travel, or rest).
|
||||
struct TimelineItemView: View {
|
||||
let item: TimelineItem
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
let isFirst: Bool
|
||||
let isLast: Bool
|
||||
|
||||
init(
|
||||
item: TimelineItem,
|
||||
games: [UUID: RichGame],
|
||||
games: [String: RichGame],
|
||||
isFirst: Bool = false,
|
||||
isLast: Bool = false
|
||||
) {
|
||||
@@ -122,7 +122,7 @@ struct TimelineItemView: View {
|
||||
|
||||
struct StopItemContent: View {
|
||||
let stop: ItineraryStop
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
|
||||
private var gamesAtStop: [RichGame] {
|
||||
stop.games.compactMap { games[$0] }
|
||||
@@ -291,7 +291,7 @@ struct TimelineGameRow: View {
|
||||
/// Full timeline view for an itinerary option.
|
||||
struct TimelineView: View {
|
||||
let option: ItineraryOption
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
|
||||
private var timeline: [TimelineItem] {
|
||||
option.generateTimeline()
|
||||
@@ -316,7 +316,7 @@ struct TimelineView: View {
|
||||
/// Horizontal scrolling timeline for compact display.
|
||||
struct HorizontalTimelineView: View {
|
||||
let option: ItineraryOption
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
|
||||
private var timeline: [TimelineItem] {
|
||||
option.generateTimeline()
|
||||
@@ -368,7 +368,7 @@ struct HorizontalTimelineView: View {
|
||||
|
||||
struct HorizontalTimelineItemView: View {
|
||||
let item: TimelineItem
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
|
||||
@@ -926,7 +926,7 @@ struct TripCreationView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func buildGamesDictionary() -> [UUID: RichGame] {
|
||||
private func buildGamesDictionary() -> [String: RichGame] {
|
||||
viewModel.availableGames.reduce(into: [:]) { $0[$1.id] = $1 }
|
||||
}
|
||||
}
|
||||
@@ -949,16 +949,16 @@ extension TripCreationViewModel.ViewState {
|
||||
|
||||
struct GamePickerSheet: View {
|
||||
let games: [RichGame]
|
||||
@Binding var selectedIds: Set<UUID>
|
||||
@Binding var selectedIds: Set<String>
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@State private var expandedSports: Set<Sport> = []
|
||||
@State private var expandedTeams: Set<UUID> = []
|
||||
@State private var expandedTeams: Set<String> = []
|
||||
|
||||
// Group games by Sport → Team (home team only to avoid duplicates)
|
||||
private var gamesBySport: [Sport: [TeamWithGames]] {
|
||||
var result: [Sport: [UUID: TeamWithGames]] = [:]
|
||||
var result: [Sport: [String: TeamWithGames]] = [:]
|
||||
|
||||
for game in games {
|
||||
let sport = game.game.sport
|
||||
@@ -1063,9 +1063,9 @@ struct GamePickerSheet: View {
|
||||
struct SportSection: View {
|
||||
let sport: Sport
|
||||
let teams: [TeamWithGames]
|
||||
@Binding var selectedIds: Set<UUID>
|
||||
@Binding var selectedIds: Set<String>
|
||||
@Binding var expandedSports: Set<Sport>
|
||||
@Binding var expandedTeams: Set<UUID>
|
||||
@Binding var expandedTeams: Set<String>
|
||||
let selectedCount: Int
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@@ -1146,8 +1146,8 @@ struct SportSection: View {
|
||||
|
||||
struct TeamSection: View {
|
||||
let teamData: TeamWithGames
|
||||
@Binding var selectedIds: Set<UUID>
|
||||
@Binding var expandedTeams: Set<UUID>
|
||||
@Binding var selectedIds: Set<String>
|
||||
@Binding var expandedTeams: Set<String>
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@@ -1310,7 +1310,7 @@ struct TeamWithGames: Identifiable {
|
||||
let sport: Sport
|
||||
var games: [RichGame]
|
||||
|
||||
var id: UUID { team.id }
|
||||
var id: String { team.id }
|
||||
|
||||
var sortedGames: [RichGame] {
|
||||
games.sorted { $0.game.dateTime < $1.game.dateTime }
|
||||
@@ -1504,7 +1504,7 @@ enum CitiesFilter: Int, CaseIterable, Identifiable {
|
||||
|
||||
struct TripOptionsView: View {
|
||||
let options: [ItineraryOption]
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
let preferences: TripPreferences?
|
||||
let convertToTrip: (ItineraryOption) -> Trip
|
||||
|
||||
@@ -1774,7 +1774,7 @@ struct TripOptionsView: View {
|
||||
|
||||
struct TripOptionCard: View {
|
||||
let option: ItineraryOption
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
let onSelect: () -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@@ -2393,7 +2393,7 @@ struct DayCell: View {
|
||||
// MARK: - Team Picker Sheet
|
||||
|
||||
struct TeamPickerSheet: View {
|
||||
@Binding var selectedTeamId: UUID?
|
||||
@Binding var selectedTeamId: String?
|
||||
let teamsBySport: [Sport: [Team]]
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@@ -12,7 +12,7 @@ struct TripDetailView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let trip: Trip
|
||||
let games: [UUID: RichGame]
|
||||
let games: [String: RichGame]
|
||||
|
||||
@State private var selectedDay: ItineraryDay?
|
||||
@State private var showExportSheet = false
|
||||
|
||||
Reference in New Issue
Block a user