// // ScheduleViewModel.swift // SportsTime // import Foundation import SwiftUI @MainActor @Observable final class ScheduleViewModel { // MARK: - Filter State var selectedSports: Set = 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: - Pre-computed Groupings (avoid computed property overhead) /// Games grouped by sport - pre-computed to avoid re-grouping on every render private(set) var gamesBySport: [(sport: Sport, games: [RichGame])] = [] /// All games matching current filters (before any display limiting) private var filteredGames: [RichGame] = [] var hasFilters: Bool { selectedSports.count < Sport.supported.count || !searchText.isEmpty } // MARK: - Actions func loadGames() async { guard !selectedSports.isEmpty else { games = [] updateFilteredGames() 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 updateFilteredGames() return } games = try await dataProvider.filterRichGames( 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 updateFilteredGames() } 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() } } // MARK: - Filtering & Grouping (pre-computed, not computed properties) /// Recomputes filtered games and groupings based on current search text func updateFilteredGames() { // Step 1: Filter by search text if searchText.isEmpty { filteredGames = games } else { let query = searchText.lowercased() filteredGames = 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) } } // Step 2: Pre-compute grouping by sport (done once, not per-render) let grouped = Dictionary(grouping: filteredGames) { $0.game.sport } gamesBySport = 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 }) } } }