Hybrid approach: keep Sport enum for existing 7 sports, add DynamicSport struct for CloudKit-defined leagues. Unified via AnySport protocol for seamless UI integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.5 KiB
6.5 KiB
Dynamic Sports via CloudKit Design
Date: 2026-01-12 Status: Draft Scope: High-level overview for scoping/prioritization
Goal
Add new sports leagues without shipping app updates. Teams, stadiums, and games for new sports sync automatically via existing CloudKit infrastructure.
Current State
Sportis a hardcoded enum with 7 sports (MLB, NBA, NHL, NFL, MLS, WNBA, NWSL)- Each sport has: rawValue, displayName, iconName, color, seasonMonths
- Teams, stadiums, and games are already CloudKit-synced
- Teams/stadiums/games store sport as a string key (e.g.,
"MLB")
Approach: Hybrid Model
- Keep
Sportenum for the 7 existing sports (compile-time safety, existing code unchanged) - Add
DynamicSportstruct for CloudKit-defined sports - Unify via
AnySportprotocol so both work interchangeably in UI
Data Flow
CloudKit "Sport" records
↓
CanonicalSyncService.syncSports()
↓
SwiftData: CanonicalSport model
↓
AppDataProvider.shared.allSports → [any AnySport]
↓
UI: SportSelectorGrid shows both enum + dynamic sports
Data Model
CloudKit Record: Sport
| Field | Type | Example |
|---|---|---|
sportId |
String | "xfl" |
abbreviation |
String | "XFL" |
displayName |
String | "XFL Football" |
iconName |
String | "football.fill" |
colorHex |
String | "#E31837" |
seasonStartMonth |
Int | 2 |
seasonEndMonth |
Int | 5 |
isActive |
Int | 1 |
SwiftData: CanonicalSport
@Model
final class CanonicalSport {
@Attribute(.unique) var id: String
var abbreviation: String
var displayName: String
var iconName: String
var colorHex: String
var seasonStartMonth: Int
var seasonEndMonth: Int
var isActive: Bool
var lastModified: Date
}
Domain: DynamicSport
struct DynamicSport: Identifiable, Hashable {
let id: String
let abbreviation: String
let displayName: String
let iconName: String
let colorHex: String
let seasonStartMonth: Int
let seasonEndMonth: Int
}
Protocol: AnySport
protocol AnySport: Identifiable, Hashable {
var sportId: String { get }
var displayName: String { get }
var iconName: String { get }
var color: Color { get }
var seasonMonths: (start: Int, end: Int) { get }
}
extension Sport: AnySport {
var sportId: String { rawValue }
// Other properties already exist on the enum
}
extension DynamicSport: AnySport {
var sportId: String { id }
var color: Color { Color(hex: colorHex) }
var seasonMonths: (start: Int, end: Int) { (seasonStartMonth, seasonEndMonth) }
}
Integration
AppDataProvider Changes
// Existing
var stadiums: [Stadium]
var teams: [Team]
// New
var dynamicSports: [DynamicSport] = []
var allSports: [any AnySport] {
let enumSports: [any AnySport] = Sport.supported
let dynamic: [any AnySport] = dynamicSports
return enumSports + dynamic
}
func sport(for id: String) -> (any AnySport)? {
// Try enum first
if let enumSport = Sport(rawValue: id) {
return enumSport
}
// Fall back to dynamic
return dynamicSports.first { $0.id == id }
}
Sync Flow
CanonicalSyncService.syncSports()fetches CloudKitSportrecords- Converts to
CanonicalSportSwiftData models - On
AppDataProvider.loadInitialData(), loads dynamic sports into memory - UI queries
allSportswhich merges enum + dynamic
UI Migration
| Component | Current | Updated |
|---|---|---|
SportSelectorGrid |
ForEach(Sport.supported) |
ForEach(dataProvider.allSports) |
sportsSection in TripCreation |
Uses Sport enum directly |
Uses any AnySport |
| Filters/pickers | Hardcoded Sport.allCases |
Uses dataProvider.allSports |
Backward Compatibility
- Existing
Sportenum untouched—no migration needed for current data - Teams/stadiums/games already store sport as
String(e.g.,"MLB") - New dynamic sports use same pattern (e.g.,
"XFL") Sport(rawValue:)returnsnilfor unknown sports → falls back to dynamic lookup
Bootstrap & Offline
- Bundled JSON includes only the 7 enum sports (no dynamic sports at first launch)
- Dynamic sports require CloudKit sync to appear
- Graceful degradation: offline users see only enum sports
File Structure
New Files
Core/Models/
├── Domain/
│ ├── DynamicSport.swift # NEW - struct for CloudKit sports
│ └── AnySport.swift # NEW - protocol unifying both
Modified Files
Core/Models/
├── Domain/
│ └── Sport.swift # Add AnySport conformance
├── CloudKit/
│ └── CKModels.swift # Add CKSport wrapper
├── Local/
│ └── CanonicalModels.swift # Add CanonicalSport model
Core/Services/
├── DataProvider.swift # Add dynamicSports, allSports
├── CanonicalSyncService.swift # Add syncSports()
└── CloudKitService.swift # Add fetchSports()
Core/Theme/
└── SportSelectorGrid.swift # Use allSports instead of Sport.supported
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Protocol name | AnySport |
Clear, follows Swift naming conventions |
| Enum sports in CloudKit? | No | Enum sports are source of truth, avoid sync conflicts |
| Icon validation | Accept any SF Symbol string | Trust CloudKit data, invalid icons show placeholder |
| Color format | Hex string (e.g., #E31837) |
Standard, easy to manage in CloudKit dashboard |
| Sort order | Enum sports first, then dynamic by displayName | Keeps familiar sports at top |
| Offline behavior | Show only enum sports | Dynamic sports require at least one successful sync |
| Discovery | Auto-show all | Any sport in CloudKit automatically appears in the app |
Not Included (YAGNI)
- Admin UI for managing sports (use CloudKit Dashboard directly)
- User-submitted sports (admin-only via CloudKit)
- Sport enable/disable toggles per user
- Sport categories or groupings
- Localized sport names (English only for now)
- Custom logo URLs (use SF Symbols only)
Dependencies
- Existing CloudKit infrastructure (already in place)
- Existing CanonicalSyncService patterns
Testing Considerations
- Test with 0 dynamic sports (offline/first launch)
- Test UI with 10+ sports (scrolling, layout)
- Test sport lookup when team references unknown sport
- Test sync adds new sport mid-session
- Test invalid SF Symbol name (should show placeholder)
- Test color parsing with various hex formats