21 KiB
TODO Bug Fixes Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Fix 9 bugs affecting app performance, UI polish, and data pipeline.
Architecture: Refactor SuggestedTripsGenerator to run heavy work off main actor; add pagination to large lists; fix UI display issues and animations; add stadium timezone support; add scraper alias.
Tech Stack: Swift, SwiftUI, @Observable, async/await, Python (scraper)
Task 1: Fix Launch Freeze (Bug 1)
Files:
- Modify:
SportsTime/Core/Services/SuggestedTripsGenerator.swift
Step 1: Identify the blocking code
The entire generateTrips() method runs on @MainActor. We need to move heavy computation to a background task while keeping UI updates on main.
Step 2: Create a non-isolated helper struct for planning
Add this struct above SuggestedTripsGenerator:
// Non-isolated helper for background trip generation
private struct TripGenerationHelper: Sendable {
let stadiums: [Stadium]
let teams: [Team]
let games: [Game]
func generateRegionalTrips(
startDate: Date,
endDate: Date
) -> [SuggestedTrip] {
let planningEngine = TripPlanningEngine()
let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 }
let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 }
var generatedTrips: [SuggestedTrip] = []
// Regional trips (copy existing logic from generateTrips)
for region in [Region.east, Region.central, Region.west] {
let regionStadiumIds = Set(
stadiums
.filter { $0.region == region }
.map { $0.id }
)
let regionGames = games.filter { regionStadiumIds.contains($0.stadiumId) }
guard !regionGames.isEmpty else { continue }
// Generate trips using existing private methods (will need to move them)
}
return generatedTrips
}
}
Step 3: Refactor generateTrips() to use Task.detached
Replace the heavy computation section in generateTrips():
func generateTrips() async {
guard !isLoading else { return }
isLoading = true
error = nil
suggestedTrips = []
loadingMessage = await loadingTextGenerator.generateMessage()
// Ensure data is loaded
if dataProvider.teams.isEmpty {
await dataProvider.loadInitialData()
}
guard !dataProvider.stadiums.isEmpty else {
error = "Unable to load stadium data"
isLoading = false
return
}
let calendar = Calendar.current
let today = Date()
guard let startDate = calendar.date(byAdding: .weekOfYear, value: 4, to: today),
let endDate = calendar.date(byAdding: .weekOfYear, value: 8, to: today) else {
error = "Failed to calculate date range"
isLoading = false
return
}
do {
let allSports = Set(Sport.supported)
let games = try await dataProvider.filterGames(
sports: allSports,
startDate: startDate,
endDate: endDate
)
guard !games.isEmpty else {
error = "No games found in the next 4-8 weeks"
isLoading = false
return
}
// Capture data for background work
let stadiums = dataProvider.stadiums
let teams = dataProvider.teams
// Heavy computation off main actor
let generatedTrips = await Task.detached(priority: .userInitiated) {
self.generateTripsInBackground(
games: games,
stadiums: stadiums,
teams: teams,
startDate: startDate,
endDate: endDate
)
}.value
suggestedTrips = generatedTrips
} catch {
self.error = "Failed to generate trips: \(error.localizedDescription)"
}
isLoading = false
}
// New nonisolated method for background work
nonisolated private func generateTripsInBackground(
games: [Game],
stadiums: [Stadium],
teams: [Team],
startDate: Date,
endDate: Date
) -> [SuggestedTrip] {
let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 }
let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 }
var generatedTrips: [SuggestedTrip] = []
// Copy existing regional trip generation logic here...
// (Move the for loop from the original generateTrips)
return generatedTrips
}
Step 4: Move helper methods to be nonisolated
Mark these methods as nonisolated:
generateRegionalTripgenerateCrossCountryTripbuildRichGamesbuildCoastToCoastRoutebuildTripStopbuildTravelSegmentsconvertToTripgenerateTripNamebuildCorridorTriphaversineDistancevalidateNoSameDayConflicts
Step 5: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Expected: Build succeeds
Step 6: Manual test
Launch app in simulator. UI should be responsive immediately while "Suggested Trips" section shows loading state.
Step 7: Commit
git add SportsTime/Core/Services/SuggestedTripsGenerator.swift
git commit -m "perf: move trip generation off main actor
Fixes launch freeze by running TripPlanningEngine in background task.
UI now responsive immediately while trips generate."
Task 2: Add Location Info to "By Game" View (Bug 3)
Files:
- Modify:
SportsTime/Features/Schedule/Views/ScheduleListView.swift
Step 1: Find GameRowView and add showLocation parameter
Locate GameRowView in the file (or it may be in a separate file). Add parameter:
struct GameRowView: View {
let game: RichGame
var showDate: Bool = false
var showLocation: Bool = false // Add this
Step 2: Add location display in the view body
After the existing content, add:
var body: some View {
VStack(alignment: .leading, spacing: 4) {
// ... existing matchup content
if showLocation {
HStack(spacing: 4) {
Image(systemName: "mappin.circle.fill")
.font(.caption2)
.foregroundStyle(.tertiary)
Text("\(game.stadium.name), \(game.stadium.city)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
Step 3: Pass showLocation when groupBy is byGame
Find where GameRowView is called and update:
// When rendering games in "by game" mode:
GameRowView(game: richGame, showDate: true, showLocation: groupBy == .byGame)
Step 4: Build and verify
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 5: Manual test
Open Schedule tab, switch to "By Game" grouping. Each row should now show stadium name and city.
Step 6: Commit
git add SportsTime/Features/Schedule/Views/ScheduleListView.swift
git commit -m "feat: show location info in By Game schedule view
Adds stadium name and city to game rows when grouped by game."
Task 3: Fix Game Selection Animation (Bug 5)
Files:
- Modify:
SportsTime/Features/Trip/Views/TripCreationView.swift
Step 1: Find the game selection tap handler
Look for the game selection list/grid and its tap action.
Step 2: Wrap selection in transaction to disable animation
// Replace direct toggle:
// viewModel.toggleGameSelection(game)
// With:
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
viewModel.toggleGameSelection(game)
}
Step 3: Alternative - add animation nil modifier
If using a selection indicator (checkmark), add:
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isSelected ? .blue : .secondary)
.animation(nil, value: isSelected)
Step 4: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 5: Manual test
Go to trip creation, select "By Game" mode, tap games to select. Animation should be instant without weird morphing.
Step 6: Commit
git add SportsTime/Features/Trip/Views/TripCreationView.swift
git commit -m "fix: remove weird animation on game selection
Disables implicit animation when toggling game selection."
Task 4: Fix Pace Capsule Animation (Bug 7)
Files:
- Modify:
SportsTime/Features/Home/Views/SavedTripsListView.swift(or similar)
Step 1: Find the pace capsule component
Search for "packed", "moderate", "relaxed" or pace-related text in the trips list view.
Step 2: Add contentTransition and disable animation
Text(trip.paceLabel) // or whatever the pace text is
.font(.caption2)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(paceColor.opacity(0.2))
.foregroundStyle(paceColor)
.clipShape(Capsule())
.contentTransition(.identity) // Prevents text morphing
.animation(nil, value: trip.id) // No animation on data change
Step 3: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 4: Manual test
View saved trips list. Pace capsules should appear without glitchy animation.
Step 5: Commit
git add SportsTime/Features/Home/Views/SavedTripsListView.swift
git commit -m "fix: pace capsule animation glitch
Adds contentTransition(.identity) to prevent text morphing."
Task 5: Remove "My Trips" Title (Bug 8)
Files:
- Modify:
SportsTime/Features/Home/Views/HomeView.swift(or main TabView file)
Step 1: Find the My Trips tab
Look for TabView and the "My Trips" tab item.
Step 2: Remove or hide navigation title
Option A - Remove .navigationTitle():
NavigationStack {
SavedTripsListView(trips: savedTrips)
// Remove: .navigationTitle("My Trips")
}
.tabItem {
Label("My Trips", systemImage: "suitcase")
}
Option B - Hide navigation bar:
NavigationStack {
SavedTripsListView(trips: savedTrips)
.toolbar(.hidden, for: .navigationBar)
}
.tabItem {
Label("My Trips", systemImage: "suitcase")
}
Step 3: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 4: Manual test
Switch to My Trips tab. No redundant title should appear above the content.
Step 5: Commit
git add SportsTime/Features/Home/Views/HomeView.swift
git commit -m "fix: remove redundant My Trips title from tab view"
Task 6: Add Stadium Timezone Support (Bug 6)
Files:
- Modify:
SportsTime/Core/Models/Domain/Stadium.swift - Modify:
SportsTime/Core/Models/Domain/Game.swift - Modify:
Scripts/sportstime_parser/normalizers/stadium_resolver.py
Step 1: Add timeZoneIdentifier to Stadium model
struct Stadium: Identifiable, Codable, Hashable {
let id: String
let name: String
let city: String
let state: String
let coordinate: CLLocationCoordinate2D
let sport: Sport
let region: Region
let timeZoneIdentifier: String? // Add this
var timeZone: TimeZone? {
timeZoneIdentifier.flatMap { TimeZone(identifier: $0) }
}
// Update init to include timeZoneIdentifier with default nil for backward compatibility
init(
id: String,
name: String,
city: String,
state: String,
coordinate: CLLocationCoordinate2D,
sport: Sport,
region: Region,
timeZoneIdentifier: String? = nil
) {
self.id = id
self.name = name
self.city = city
self.state = state
self.coordinate = coordinate
self.sport = sport
self.region = region
self.timeZoneIdentifier = timeZoneIdentifier
}
}
Step 2: Add localGameTime to RichGame
extension RichGame {
var localGameTime: String {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a z"
formatter.timeZone = stadium.timeZone ?? .current
return formatter.string(from: game.dateTime)
}
var localGameTimeShort: String {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
formatter.timeZone = stadium.timeZone ?? .current
return formatter.string(from: game.dateTime)
}
}
Step 3: Update Python scraper StadiumInfo
In stadium_resolver.py, update the dataclass and all stadium entries:
@dataclass
class StadiumInfo:
canonical_id: str
name: str
city: str
state: str
country: str
sport: str
latitude: float
longitude: float
timezone: str = "America/New_York" # Add with default
# Update entries, e.g.:
"stadium_nba_chase_center": StadiumInfo("stadium_nba_chase_center", "Chase Center", "San Francisco", "CA", "USA", "nba", 37.7680, -122.3877, "America/Los_Angeles"),
Step 4: Build Swift code
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 5: Commit
git add SportsTime/Core/Models/Domain/Stadium.swift SportsTime/Core/Models/Domain/Game.swift Scripts/sportstime_parser/normalizers/stadium_resolver.py
git commit -m "feat: add timezone support for stadium-local game times
Adds timeZoneIdentifier to Stadium and localGameTime to RichGame.
Game times can now display in venue local time."
Note: Full timezone population for all stadiums is a separate data task.
Task 7: Add Frost Bank Center Alias (Bug 9)
Files:
- Modify:
Scripts/stadium_aliases.json
Step 1: Open stadium_aliases.json and add alias
Add entry for AT&T Center (old name for Frost Bank Center):
{
"canonical_id": "stadium_nba_frost_bank_center",
"alias_name": "AT&T Center",
"sport": "nba",
"valid_from": "2002-10-18",
"valid_to": "2024-07-01",
"notes": "Renamed to Frost Bank Center in July 2024"
}
Step 2: Verify JSON is valid
Run:
python3 -c "import json; json.load(open('Scripts/stadium_aliases.json'))"
Expected: No output (valid JSON)
Step 3: Test scraper resolution
Run:
cd Scripts && python3 -c "
from sportstime_parser.normalizers.stadium_resolver import resolve_stadium
result = resolve_stadium('nba', 'AT&T Center')
print(f'Resolved: {result.canonical_id}, confidence: {result.confidence}')
"
Expected: Resolved: stadium_nba_frost_bank_center, confidence: 95
Step 4: Commit
git add Scripts/stadium_aliases.json
git commit -m "fix: add AT&T Center alias for Frost Bank Center
NBA scraper now resolves old stadium name to current Spurs arena."
Task 8: Add Pagination to Schedule List (Bug 2)
Files:
- Modify:
SportsTime/Features/Schedule/Views/ScheduleListView.swift - Modify:
SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift
Step 1: Add pagination state to ViewModel
@Observable
class ScheduleViewModel {
// Existing properties...
// Pagination
private let pageSize = 50
var displayedGames: [RichGame] = []
private var currentPage = 0
private var allFilteredGames: [RichGame] = []
var hasMoreGames: Bool {
displayedGames.count < allFilteredGames.count
}
func loadInitialGames() {
currentPage = 0
displayedGames = Array(allFilteredGames.prefix(pageSize))
}
func loadMoreGames() {
guard hasMoreGames else { return }
currentPage += 1
let start = currentPage * pageSize
let end = min(start + pageSize, allFilteredGames.count)
displayedGames.append(contentsOf: allFilteredGames[start..<end])
}
}
Step 2: Update filtering to populate allFilteredGames
In the method that filters/fetches games:
func applyFilters() async {
// ... existing filter logic that produces games
allFilteredGames = filteredGames // Store full list
loadInitialGames() // Display first page
}
Step 3: Update ScheduleListView to use displayedGames
struct ScheduleListView: View {
@Bindable var viewModel: ScheduleViewModel
var body: some View {
List {
ForEach(viewModel.displayedGames) { game in
GameRowView(game: game, showDate: true, showLocation: viewModel.groupBy == .byGame)
.onAppear {
if game.id == viewModel.displayedGames.last?.id {
viewModel.loadMoreGames()
}
}
}
if viewModel.hasMoreGames {
ProgressView()
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
}
}
}
Step 4: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 5: Manual test
Open Schedule tab with "By Game" grouping. List should load quickly with 50 games, then load more as you scroll.
Step 6: Commit
git add SportsTime/Features/Schedule/Views/ScheduleListView.swift SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift
git commit -m "perf: add pagination to schedule list
Loads 50 games at a time to fix lag with large datasets."
Task 9: Fix Game Selection View Performance (Bug 4)
Files:
- Modify:
SportsTime/Features/Trip/Views/TripCreationView.swift - Modify:
SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift
Step 1: Add pagination to TripCreationViewModel
Similar to Task 8, add pagination for the game selection:
// In TripCreationViewModel
private let gamePageSize = 50
var displayedAvailableGames: [RichGame] = []
private var currentGamePage = 0
var hasMoreAvailableGames: Bool {
displayedAvailableGames.count < availableGames.count
}
func loadInitialAvailableGames() {
currentGamePage = 0
displayedAvailableGames = Array(availableGames.prefix(gamePageSize))
}
func loadMoreAvailableGames() {
guard hasMoreAvailableGames else { return }
currentGamePage += 1
let start = currentGamePage * gamePageSize
let end = min(start + gamePageSize, availableGames.count)
displayedAvailableGames.append(contentsOf: availableGames[start..<end])
}
Step 2: Update game selection view to use displayedAvailableGames
// In game selection section of TripCreationView
LazyVStack(spacing: 8) {
ForEach(viewModel.displayedAvailableGames) { game in
GameSelectionRow(game: game, isSelected: viewModel.selectedGameIds.contains(game.id))
.onTapGesture {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
viewModel.toggleGameSelection(game)
}
}
.onAppear {
if game.id == viewModel.displayedAvailableGames.last?.id {
viewModel.loadMoreAvailableGames()
}
}
}
}
Step 3: Fix color inconsistency
Ensure all game rows use consistent background:
struct GameSelectionRow: View {
let game: RichGame
let isSelected: Bool
var body: some View {
HStack {
// Game info...
Spacer()
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isSelected ? .blue : .secondary)
}
.padding()
.background(Color(.systemBackground)) // Consistent background
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
Step 4: Build and test
Run:
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
Step 5: Manual test
Create a trip with "By Game" mode. Game selection should be smooth and colors consistent.
Step 6: Commit
git add SportsTime/Features/Trip/Views/TripCreationView.swift SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift
git commit -m "perf: add pagination to game selection view
Fixes lag and color inconsistency in trip creation game selection."
Final Task: Run Full Test Suite
Step 1: Run all tests
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
Step 2: Verify all pass
Expected: All tests pass (existing 63 tests)
Step 3: Manual smoke test
- Launch app - UI responsive immediately
- Home tab - suggested trips load in background
- Schedule tab - "By Game" shows location, scrolls smoothly
- Trip creation - game selection smooth, no weird animation
- My Trips - no redundant title, pace capsules look good