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
This commit is contained in:
Arkadiusz Fal
2026-07-05 21:06:43 +02:00
parent b632f11b2f
commit 6fed1b0ca7
4 changed files with 55 additions and 1 deletions

View File

@@ -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
}
}

View File

@@ -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 }

View File

@@ -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 {

View File

@@ -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)