- Add allRichGames method to DataProvider - Update TripCreationViewModel.loadGamesForBrowsing to use allGames (removes 90-day limit) - Update MockCloudKitService sync methods to use new delta sync signatures - Update MockAppDataProvider with renamed methods and new allGames/allRichGames - Fix all callers: ScheduleViewModel, TripCreationViewModel, SuggestedTripsGenerator, GameMatcher - Update CLAUDE.md documentation with new method names This completes the delta sync implementation: - CloudKit sync now uses modificationDate for proper delta sync - First sync fetches ALL data, subsequent syncs only fetch modified records - "By Games" mode now shows all available games (not just 90 days) - All data types (stadiums, teams, games) use consistent delta sync pattern Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & Run Commands
# Build the iOS app
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build
# Run tests
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
# Run specific test suite
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TripPlanningEngineTests test
# Run a single test
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TestClassName/testMethodName test
# Data scraping (Python)
cd Scripts && pip install -r requirements.txt
python scrape_schedules.py --sport all --season 2026
Architecture Overview
This is an iOS app for planning multi-stop sports road trips. It uses Clean MVVM with feature-based modules.
Three-Layer Architecture
-
Presentation Layer (
Features/): SwiftUI Views + @Observable ViewModels organized by feature (Home, Trip, Schedule, Settings) -
Domain Layer (
Planning/): Trip planning logicTripPlanningEngine- Main orchestrator, 7-step algorithmRouteOptimizer- TSP solver (exact for <8 stops, heuristic otherwise)ScheduleMatcher- Finds games along route corridorTripScorer- Multi-factor scoring (game quality, route efficiency, leisure balance)
-
Data Layer (
Core/):Models/Domain/- Pure Swift structs (Trip, Game, Stadium, Team)Models/CloudKit/- CKRecord wrappers for public databaseModels/Local/- SwiftData models for local persistence (SavedTrip, UserPreferences)Services/- CloudKitService (schedules), LocationService (geocoding/routing)
-
Export Layer (
Export/):PDFGenerator- Generates PDF trip itineraries with maps, photos, and attractionsExportService- Orchestrates PDF export with asset prefetchingServices/MapSnapshotService- Generates static map images via MKMapSnapshotterServices/RemoteImageService- Downloads/caches team logos and stadium photosServices/POISearchService- Finds nearby restaurants, attractions via MKLocalSearchServices/PDFAssetPrefetcher- Parallel prefetching of all PDF assets
Data Architecture (Offline-First)
CRITICAL: AppDataProvider.shared is the ONLY source of truth for canonical data.
All code that reads stadiums, teams, games, or league structure MUST use AppDataProvider.shared. Never access CloudKit or SwiftData directly for this data.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Bundled JSON │ │ SwiftData │ │ CloudKit │
│ (App Bundle) │ │ (Local Store) │ │ (Remote Sync) │
└────────┬─────────┘ └────────▲─────────┘ └────────┬─────────┘
│ │ │
│ Bootstrap │ Read │ Background
│ (first launch) │ │ Sync
▼ │ ▼
┌─────────────────────────────────┴────────────────────────────────────┐
│ AppDataProvider.shared │
│ (Single Source of Truth) │
└─────────────────────────────────┬────────────────────────────────────┘
│
▼
All Features, ViewModels, Services
App Startup Flow:
- Bootstrap (first launch only):
BootstrapServiceloads bundled JSON → SwiftData - Configure:
AppDataProvider.shared.configure(with: context) - Load:
AppDataProvider.shared.loadInitialData()reads SwiftData into memory - App usable immediately with local data
- Background sync:
CanonicalSyncService.syncAll()fetches CloudKit → updates SwiftData (non-blocking) - Reload: After sync completes,
loadInitialData()refreshes in-memory cache
Offline Handling:
- SwiftData always has data (from bootstrap or last successful CloudKit sync)
- If CloudKit unavailable, app continues with existing local data
- First launch + offline = bootstrap data used
Canonical Data Models (in SwiftData, synced from CloudKit):
CanonicalStadium→Stadium(domain)CanonicalTeam→Team(domain)CanonicalGame→Game(domain)LeagueStructureModelTeamAlias,StadiumAlias
User Data Models (local only, not synced):
SavedTrip,StadiumVisit,UserPreferences,Achievement
Correct Usage:
// ✅ CORRECT - Use AppDataProvider
let stadiums = AppDataProvider.shared.stadiums
let teams = AppDataProvider.shared.teams
let games = try await AppDataProvider.shared.filterGames(sports: sports, startDate: start, endDate: end)
let richGames = try await AppDataProvider.shared.filterRichGames(...)
let allGames = try await AppDataProvider.shared.allGames(for: sports)
// ❌ WRONG - Never access CloudKit directly for reads
let stadiums = try await CloudKitService.shared.fetchStadiums() // NO!
// ❌ WRONG - Never fetch canonical data from SwiftData directly
let descriptor = FetchDescriptor<CanonicalStadium>()
let stadiums = try context.fetch(descriptor) // NO! (except in DataProvider/Sync/Bootstrap)
Allowed Direct SwiftData Access:
DataProvider.swift- It IS the data providerCanonicalSyncService.swift- CloudKit → SwiftData syncBootstrapService.swift- Initial data populationStadiumIdentityService.swift- Specialized identity resolution for stadium renames- User data (e.g.,
StadiumVisit) - Not canonical data
Key Data Flow
TripCreationView → TripCreationViewModel → PlanningRequest
→ TripPlanningEngine (ScheduleMatcher + RouteOptimizer + TripScorer)
→ PlanningResult → Trip → TripDetailView → SavedTrip (persist)
Important Patterns
- ViewModels use
@Observable(not ObservableObject) - All planning engine components are
actortypes for thread safety - Domain models are pure Codable structs; SwiftData models wrap them via encoded
Datafields - CloudKit container ID:
iCloud.com.sportstime.app PDFGeneratorandExportServiceare@MainActor final class(not actors) because they access MainActor-isolated UI properties and use UIKit drawing
iOS 26 API Notes
Deprecated APIs (use with @available(iOS, deprecated: 26.0) annotation):
CLGeocoder→ UseMKLocalSearchwith.addressresult type insteadMKPlacemarkproperties (locality, administrativeArea, etc.) → Still work but deprecated; useMKMapItemproperties where possibleMKMapItem.locationis non-optional in iOS 26 (returnsCLLocation, notCLLocation?)
Swift 6 Concurrency:
- Use
@retroactivefor protocol conformances on types you don't own (e.g.,CLLocationCoordinate2D: @retroactive Codable) - When capturing
varinasync let, create immutable copies first to avoid Swift 6 warnings
Key View Components
TripDetailView (Features/Trip/Views/TripDetailView.swift)
Displays trip itinerary with conflict detection for same-day games in different cities.
Conflict Detection System:
detectConflicts(for: ItineraryDay)- Checks if multiple stops have games on the same calendar day- Returns
DayConflictInfowithhasConflict,conflictingStops, andconflictingCities
RouteOptionsCard (Expandable):
- Shows when multiple route options exist for the same day (conflicting games in different cities)
- Collapsed: Shows "N route options" with city list, tap to expand
- Expanded: Shows each option as a
RouteOptionCardwith numbered badge (Option 1, Option 2, etc.) - Single routes (no conflict): Uses regular
DayCard, auto-expanded
RouteOptionCard:
- Individual option within the expandable RouteOptionsCard
- Shows option number badge, city name, games at that stop, and travel info
DayCard Component (non-conflict mode):
specificStop: TripStop?- When provided, shows only that stop's gamesprimaryCityForDay- Returns the city for the cardgamesOnThisDay- Returns games filtered to the calendar day
Visual Design:
- Expandable cards have orange border and branch icon
- Option badges are blue capsules
- Chevron indicates expand/collapse state
Scripts
Scripts/scrape_schedules.py scrapes NBA/MLB/NHL schedules from multiple sources (Basketball-Reference, Baseball-Reference, Hockey-Reference, official APIs) for cross-validation. See Scripts/DATA_SOURCES.md for source URLs and rate limits.
Documentation
The docs/ directory contains project documentation:
MARKET_RESEARCH.md- Competitive analysis and feature recommendations based on sports travel app market research (January 2026)
Test Suites
- TripPlanningEngineTests (50 tests) - Routing logic, must-see games, required destinations, EV charging, edge cases
- DayCardTests (11 tests) - DayCard conflict detection, warning display, stop filtering, edge cases
- DuplicateGameIdTests (2 tests) - Regression tests for handling duplicate game IDs in JSON data
Bug Fix Protocol
Whenever fixing a bug:
- Write a regression test that reproduces the bug before fixing it
- Include edge cases - test boundary conditions, null/empty inputs, and related scenarios
- Confirm all tests pass by running the test suite before considering the fix complete
- Name tests descriptively - e.g.,
test_DayCard_OnlyShowsGamesFromPrimaryStop_WhenMultipleStopsOverlapSameDay
Example workflow:
# 1. Write failing test that reproduces the bug
# 2. Fix the bug
# 3. Verify the new test passes along with all existing tests
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test
Future Phases
See docs/MARKET_RESEARCH.md for full competitive analysis and feature prioritization.
Phase 2: AI-Powered Trip Planning
Natural Language Trip Planning
- Allow users to describe trips in plain English: "plan me a baseball trip from Texas" or "I want to see the Yankees and Red Sox in one weekend"
- Parse intent, extract constraints (sports, dates, locations, budget)
- Generate trip suggestions from natural language input
On-Device Intelligence (Apple Foundation Models)
- Use Apple's Foundation Models framework (iOS 26+) for on-device AI processing
- Privacy-preserving - no data leaves the device
- Features to enable:
- Smart trip suggestions based on user history
- Natural language query understanding
- Personalized game recommendations
- Conversational trip refinement ("add another game" / "make it shorter")
Implementation Notes:
- Foundation Models requires iOS 26+ and Apple Silicon
- Use
@Generablefor structured output parsing - Implement graceful fallback for unsupported devices
- See
axiom:axiom-foundation-modelsskill for patterns
Phase 3: Stadium Bucket List
Progress Tracking
- Visual map showing visited vs. remaining stadiums per league
- Digital passport/stamps for each visited stadium
- Achievement badges (e.g., "All NL West", "Coast to Coast", "10 Stadiums")
- Shareable progress cards for social media
Competitors: Baseball Bucket List, Sports Venue Tracker, MLB BallPark Pass-Port (physical)
Phase 4: Group Trip Coordination
Collaborative Planning
- Invite friends to collaborate on trip planning
- Polling/voting on game choices and destinations
- Expense splitting integration
- Shared itinerary with real-time sync
- Role delegation (lodging, tickets, restaurants)
Competitors: SquadTrip, Troupe, Howbout
Phase 5: Fan Community
Social Features
- Stadium tips from locals (best food, parking, pre-game bars)
- Fan meetup coordination for away games
- Trip reviews and ratings
- Discussion forums for specific stadiums
Competitor: Fantrip (fan-to-fan stays and local tips)