Files
Sportstime/SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift
Trey t 5ed4e309bd feat(schedule): group games by sport instead of date
Games in schedule view now display in sport sections (MLB, NBA, etc.)
with games sorted by date within each section. Each game row shows its
date since the section header now shows sport instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 17:36:22 -06:00

148 lines
4.2 KiB
Swift

//
// ScheduleViewModel.swift
// SportsTime
//
import Foundation
import SwiftUI
@MainActor
@Observable
final class ScheduleViewModel {
// MARK: - Filter State
var selectedSports: Set<Sport> = Set(Sport.supported)
var startDate: Date = Date()
var endDate: Date = Calendar.current.date(byAdding: .day, value: 14, to: Date()) ?? Date()
var searchText: String = ""
// MARK: - Data State
private(set) var games: [RichGame] = []
private(set) var isLoading = false
private(set) var error: Error?
private(set) var errorMessage: String?
private let dataProvider = AppDataProvider.shared
// MARK: - Computed Properties
var filteredGames: [RichGame] {
guard !searchText.isEmpty else { return games }
let query = searchText.lowercased()
return games.filter { game in
game.homeTeam.name.lowercased().contains(query) ||
game.homeTeam.city.lowercased().contains(query) ||
game.awayTeam.name.lowercased().contains(query) ||
game.awayTeam.city.lowercased().contains(query) ||
game.stadium.name.lowercased().contains(query) ||
game.stadium.city.lowercased().contains(query)
}
}
var gamesByDate: [(date: Date, games: [RichGame])] {
let calendar = Calendar.current
let grouped = Dictionary(grouping: filteredGames) { game in
calendar.startOfDay(for: game.game.dateTime)
}
return grouped.sorted { $0.key < $1.key }.map { ($0.key, $0.value) }
}
var gamesBySport: [(sport: Sport, games: [RichGame])] {
let grouped = Dictionary(grouping: filteredGames) { game in
game.game.sport
}
// Sort by sport order (use allCases index for consistent ordering)
// Within each sport, games are sorted by date
return grouped
.sorted { lhs, rhs in
let lhsIndex = Sport.allCases.firstIndex(of: lhs.key) ?? 0
let rhsIndex = Sport.allCases.firstIndex(of: rhs.key) ?? 0
return lhsIndex < rhsIndex
}
.map { (sport: $0.key, games: $0.value.sorted { $0.game.dateTime < $1.game.dateTime }) }
}
var hasFilters: Bool {
selectedSports.count < Sport.supported.count || !searchText.isEmpty
}
// MARK: - Actions
func loadGames() async {
guard !selectedSports.isEmpty else {
games = []
return
}
isLoading = true
error = nil
errorMessage = nil
do {
// Load initial data if needed
if dataProvider.teams.isEmpty {
await dataProvider.loadInitialData()
}
// Check if data provider had an error
if let providerError = dataProvider.errorMessage {
self.errorMessage = providerError
self.error = dataProvider.error
isLoading = false
return
}
games = try await dataProvider.fetchRichGames(
sports: selectedSports,
startDate: startDate,
endDate: endDate
)
} catch let cloudKitError as CloudKitError {
self.error = cloudKitError
self.errorMessage = cloudKitError.errorDescription
} catch {
self.error = error
self.errorMessage = error.localizedDescription
}
isLoading = false
}
func clearError() {
error = nil
errorMessage = nil
}
func toggleSport(_ sport: Sport) {
if selectedSports.contains(sport) {
selectedSports.remove(sport)
} else {
selectedSports.insert(sport)
}
Task {
await loadGames()
}
}
func resetFilters() {
selectedSports = Set(Sport.supported)
searchText = ""
startDate = Date()
endDate = Calendar.current.date(byAdding: .day, value: 14, to: Date()) ?? Date()
Task {
await loadGames()
}
}
func updateDateRange(start: Date, end: Date) {
startDate = start
endDate = end
Task {
await loadGames()
}
}
}