From 6fed1b0ca7861480be94c67dec63d9c8e396fca5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 5 Jul 2026 21:06:43 +0200 Subject: [PATCH] Enforce theme at the window level instead of preferredColorScheme SwiftUI's .preferredColorScheme on the root view could not revert from dark back to light/system while a sheet was presented, and on macOS it never applied to secondary window scenes such as Settings. The override is now applied directly to UIKit/AppKit instead. - SettingsManager.applyTheme sets overrideUserInterfaceStyle on every window of every connected scene (iOS) or NSApp.appearance (macOS); the theme setter calls it, so any future code path that changes the theme outside the setter must do the same - AppTheme gains userInterfaceStyle / appearance mappings, with .system resolving to .unspecified / nil so the OS setting takes over again - Applied on app appear for the initial launch state, and after both iCloud sync paths, since theme can arrive as a synced change and is no longer driven by SwiftUI state Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o --- .../Settings/SettingsManager+CloudSync.swift | 10 ++++++++ .../Settings/SettingsManager+General.swift | 19 +++++++++++++++ Yattee/Core/Settings/SettingsTypes.swift | 23 +++++++++++++++++++ Yattee/YatteeApp.swift | 4 +++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Yattee/Core/Settings/SettingsManager+CloudSync.swift b/Yattee/Core/Settings/SettingsManager+CloudSync.swift index d2eea94d..af7f912d 100644 --- a/Yattee/Core/Settings/SettingsManager+CloudSync.swift +++ b/Yattee/Core/Settings/SettingsManager+CloudSync.swift @@ -389,6 +389,10 @@ extension SettingsManager { clearCache() updateLastSyncTime() + + #if !os(tvOS) + Self.applyTheme(theme) + #endif } /// Refreshes settings from iCloud by copying iCloud values to local storage. @@ -445,5 +449,11 @@ extension SettingsManager { // Clear caches to force re-read from local storage clearCache() + + // Re-apply the theme in case it was among the synced changes — + // it is enforced at the window level, not via SwiftUI state. + #if !os(tvOS) + Self.applyTheme(theme) + #endif } } diff --git a/Yattee/Core/Settings/SettingsManager+General.swift b/Yattee/Core/Settings/SettingsManager+General.swift index 9d9eb730..89e76763 100644 --- a/Yattee/Core/Settings/SettingsManager+General.swift +++ b/Yattee/Core/Settings/SettingsManager+General.swift @@ -24,9 +24,28 @@ extension SettingsManager { set { _theme = newValue set(newValue.rawValue, for: .theme) + Self.applyTheme(newValue) } } + /// Forces the theme onto the platform windows directly. SwiftUI's + /// `.preferredColorScheme` fails to revert from dark back to light/system + /// while a sheet is presented, and on macOS it never covered secondary + /// window scenes (Settings), so the override is applied at the + /// UIKit/AppKit level instead. + static func applyTheme(_ theme: AppTheme) { + #if os(iOS) + for scene in UIApplication.shared.connectedScenes { + guard let windowScene = scene as? UIWindowScene else { continue } + for window in windowScene.windows { + window.overrideUserInterfaceStyle = theme.userInterfaceStyle + } + } + #elseif os(macOS) + NSApp.appearance = theme.appearance + #endif + } + var accentColor: AccentColor { get { if let cached = _accentColor { return cached } diff --git a/Yattee/Core/Settings/SettingsTypes.swift b/Yattee/Core/Settings/SettingsTypes.swift index 917e011d..530e657e 100644 --- a/Yattee/Core/Settings/SettingsTypes.swift +++ b/Yattee/Core/Settings/SettingsTypes.swift @@ -7,6 +7,11 @@ import Foundation import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif // MARK: - Theme & Appearance @@ -22,6 +27,24 @@ enum AppTheme: String, CaseIterable, Codable { case .dark: return .dark } } + + #if canImport(UIKit) + var userInterfaceStyle: UIUserInterfaceStyle { + switch self { + case .system: return .unspecified + case .light: return .light + case .dark: return .dark + } + } + #elseif canImport(AppKit) + var appearance: NSAppearance? { + switch self { + case .system: return nil + case .light: return NSAppearance(named: .aqua) + case .dark: return NSAppearance(named: .darkAqua) + } + } + #endif } enum AccentColor: String, CaseIterable, Codable { diff --git a/Yattee/YatteeApp.swift b/Yattee/YatteeApp.swift index ee8e909a..9dd6dfd1 100644 --- a/Yattee/YatteeApp.swift +++ b/Yattee/YatteeApp.swift @@ -70,7 +70,6 @@ struct YatteeApp: App { ContentView() .appEnvironment(appEnvironment) #if !os(tvOS) - .preferredColorScheme(appEnvironment.settingsManager.theme.colorScheme) .tint(appEnvironment.settingsManager.resolvedAccentColor) #endif #if os(macOS) @@ -85,6 +84,9 @@ struct YatteeApp: App { handleContinuedActivity(activity) } .onAppear { + #if !os(tvOS) + SettingsManager.applyTheme(appEnvironment.settingsManager.theme) + #endif registerBackgroundTasksIfNeeded() #if os(tvOS) TopShelfSnapshotWriter.startObserving(dataManager: appEnvironment.dataManager)