import BackgroundTasks import SwiftUI import UIKit @main struct PSUScheduleApp: App { @State private var model = AppModel() @Environment(\.scenePhase) private var phase var body: some Scene { WindowGroup { RootView() .environment(model) // The theme is set on the window, not with preferredColorScheme: sheets that are // already open (the settings themselves) switch along with the screens behind them. .onChange(of: model.settings.theme, initial: true) { old, new in Theme.apply(new, animated: old != new) } .task { Background.schedule() } } .onChange(of: phase) { _, now in // Back in the app after a while: a quiet refresh, as the Android client does on start. if now == .active, model.settings.current != nil { Task { await model.refresh() } } if now == .background { Background.schedule() } } // Every few hours iOS wakes the app to refresh the timetable, widgets and reminders. .backgroundTask(.appRefresh(Background.identifier)) { await Background.refresh() } } } enum Theme { static func apply(_ mode: ThemeMode, animated: Bool) { let style: UIUserInterfaceStyle = switch mode { case .system: .unspecified case .light: .light case .dark: .dark } for scene in UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }) { for window in scene.windows where window.overrideUserInterfaceStyle != style { if animated { UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve) { window.overrideUserInterfaceStyle = style } } else { window.overrideUserInterfaceStyle = style } } } } } enum Background { static let identifier = "org.xtimms.psu.schedule.refresh" static func schedule() { let request = BGAppRefreshTaskRequest(identifier: identifier) request.earliestBeginDate = Date().addingTimeInterval(6 * 3600) try? BGTaskScheduler.shared.submit(request) } @MainActor static func refresh() async { schedule() let model = AppModel() let known = Set(model.allChanges.map(\.at)) await model.refresh() // A change found by this run, for the open group: tell the student without waiting for // them to open the app. The overview's card shows the same record. if model.settings.changeAlerts, let record = model.changes, !record.seen, !known.contains(record.at) { await ChangeAlerts.show(record) } } }