800 lines
21 KiB
Markdown
800 lines
21 KiB
Markdown
# 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`:
|
|
|
|
```swift
|
|
// 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()`:
|
|
|
|
```swift
|
|
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`:
|
|
- `generateRegionalTrip`
|
|
- `generateCrossCountryTrip`
|
|
- `buildRichGames`
|
|
- `buildCoastToCoastRoute`
|
|
- `buildTripStop`
|
|
- `buildTravelSegments`
|
|
- `convertToTrip`
|
|
- `generateTripName`
|
|
- `buildCorridorTrip`
|
|
- `haversineDistance`
|
|
- `validateNoSameDayConflicts`
|
|
|
|
**Step 5: Build and test**
|
|
|
|
Run:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
// When rendering games in "by game" mode:
|
|
GameRowView(game: richGame, showDate: true, showLocation: groupBy == .byGame)
|
|
```
|
|
|
|
**Step 4: Build and verify**
|
|
|
|
Run:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
|
.foregroundStyle(isSelected ? .blue : .secondary)
|
|
.animation(nil, value: isSelected)
|
|
```
|
|
|
|
**Step 4: Build and test**
|
|
|
|
Run:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```swift
|
|
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:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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()`:
|
|
```swift
|
|
NavigationStack {
|
|
SavedTripsListView(trips: savedTrips)
|
|
// Remove: .navigationTitle("My Trips")
|
|
}
|
|
.tabItem {
|
|
Label("My Trips", systemImage: "suitcase")
|
|
}
|
|
```
|
|
|
|
Option B - Hide navigation bar:
|
|
```swift
|
|
NavigationStack {
|
|
SavedTripsListView(trips: savedTrips)
|
|
.toolbar(.hidden, for: .navigationBar)
|
|
}
|
|
.tabItem {
|
|
Label("My Trips", systemImage: "suitcase")
|
|
}
|
|
```
|
|
|
|
**Step 3: Build and test**
|
|
|
|
Run:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```swift
|
|
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**
|
|
|
|
```swift
|
|
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:
|
|
|
|
```python
|
|
@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:
|
|
```bash
|
|
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
|
|
```
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
```bash
|
|
python3 -c "import json; json.load(open('Scripts/stadium_aliases.json'))"
|
|
```
|
|
|
|
Expected: No output (valid JSON)
|
|
|
|
**Step 3: Test scraper resolution**
|
|
|
|
Run:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```swift
|
|
@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:
|
|
|
|
```swift
|
|
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**
|
|
|
|
```swift
|
|
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:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```swift
|
|
// 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**
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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:
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
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
|