// // ScoreboardViewModel.swift // IceGlass-iOS // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import Foundation import Observation /// Bridges MainService → SwiftUI. Re-published whenever data lands so views /// re-render. Owns refresh kick-off on scene activation and pull-to-refresh. @Observable @MainActor final class ScoreboardViewModel { /// Bumped on every observer callback; views read it implicitly via the /// Observation framework so the entire tree re-evaluates. private var revision: Int = 0 /// Tracks the most recent successful fetch time for the "Updated …" header. var lastUpdated: Date? /// Toggled by pull-to-refresh; views can show a spinner. var isRefreshing: Bool = false /// Settings — surfaced separately so the settings sheet can bind directly. var displayOption: AppSettings.DisplayOption { get { _ = revision return AppSettings.shared.displayOption } set { AppSettings.shared.displayOption = newValue revision += 1 } } var gamesByDate: [Scoreboard.GameDay] { _ = revision return MainService.shared.gamesByDate } var currentRoundSeriesItems: [MainService.RoundSeriesItem] { _ = revision return MainService.shared.currentRoundSeriesItems } var currentRoundNumber: Int? { _ = revision return MainService.shared.bracket?.currentRound } /// Bridge object that forwards MainService callbacks back to this view model. /// Held strongly so MainService's weak observer reference stays alive. private var bridge: ObserverBridge? init() {} /// Wire this view model up to MainService. Idempotent; called from `.onAppear`. func attach() { guard bridge == nil else { return } let bridge = ObserverBridge { [weak self] in self?.handleUpdate() } self.bridge = bridge MainService.shared.observer = bridge // Initial sync — MainService may have already loaded a cached snapshot. handleUpdate() } private func handleUpdate() { lastUpdated = MainService.shared.lastUpdated revision += 1 } func refreshNow() async { isRefreshing = true await MainService.shared.fetchAll() isRefreshing = false } func handleScenePhaseActive() { MainService.shared.resumePolling() Task { await refreshNow() } } func handleScenePhaseInactive() { MainService.shared.suspendPolling() } } /// Class adapter so MainService's `weak var observer` has something to hold; /// @Observable view-model classes don't compose cleanly with weak refs. @MainActor private final class ObserverBridge: MainServiceObserver { private let onUpdate: () -> Void init(onUpdate: @escaping () -> Void) { self.onUpdate = onUpdate } func mainServiceDidUpdate() { onUpdate() } }