diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 212966c0f..77eb4cd31 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; }; 6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; }; 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; - 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; }; DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; @@ -71,6 +70,7 @@ DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; }; DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; }; + DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; }; DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; }; DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; }; DD485F142E454B2600CE8CBF /* SecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD485F132E454B2600CE8CBF /* SecureMessenger.swift */; }; @@ -434,7 +434,6 @@ 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = ""; }; 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = ""; }; 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; - 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; @@ -472,6 +471,7 @@ DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = ""; }; + DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = ""; }; DD485F132E454B2600CE8CBF /* SecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureMessenger.swift; sourceTree = ""; }; @@ -1538,6 +1538,7 @@ DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */, 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */, DD4A407D2E6AFEE6007B318B /* AuthService.swift */, + DD1D52BF2E4C100000000001 /* AppearanceMode.swift */, DD1D52B82E1EB5DC00432050 /* TabPosition.swift */, DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */, DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */, @@ -2144,6 +2145,7 @@ DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */, DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */, DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */, + DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */, DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, 654132EA2E19F24800BDBE08 /* TOTPGenerator.swift in Sources */, 6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */, @@ -2211,7 +2213,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -2238,7 +2240,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; diff --git a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift index 77cf76eed..17d5b73c1 100644 --- a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift +++ b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift @@ -40,6 +40,6 @@ struct AddAlarmSheet: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } } diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 145d345a1..144edb8a9 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -41,7 +41,7 @@ struct AlarmEditor: View { } .navigationTitle(alarm.type.rawValue) } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } @ViewBuilder diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 4dc315835..3240208db 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -90,7 +90,7 @@ struct AlarmListView: View { Button { sheetInfo = .picker } label: { Image(systemName: "plus") } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } // MARK: - Views diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 85df90ff0..3f328bfa4 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -194,7 +194,7 @@ struct AlarmSettingsView: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Alarm Settings", displayMode: .inline) } } diff --git a/LoopFollow/Alarm/AlarmsContainerView.swift b/LoopFollow/Alarm/AlarmsContainerView.swift index 9eb24db23..dea4ce515 100644 --- a/LoopFollow/Alarm/AlarmsContainerView.swift +++ b/LoopFollow/Alarm/AlarmsContainerView.swift @@ -17,6 +17,6 @@ struct AlarmsContainerView: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } } diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index 3cf905627..a50bc6ea9 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -27,7 +27,7 @@ struct BackgroundRefreshSettingsView: View { stopTimer() } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Background Refresh Settings", displayMode: .inline) } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 82f17a602..290e1fb03 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -64,10 +64,13 @@ extension MainViewController { LoopStatusLabel.textAlignment = .right LoopStatusLabel.font = UIFont.systemFont(ofSize: 17) - if Storage.shared.forceDarkMode.value { + switch Storage.shared.appearanceMode.value { + case .dark: LoopStatusLabel.textColor = UIColor.white - } else { + case .light: LoopStatusLabel.textColor = UIColor.black + case .system: + LoopStatusLabel.textColor = UIColor.label } } } diff --git a/LoopFollow/Helpers/AppearanceMode.swift b/LoopFollow/Helpers/AppearanceMode.swift new file mode 100644 index 000000000..2e3c7bc64 --- /dev/null +++ b/LoopFollow/Helpers/AppearanceMode.swift @@ -0,0 +1,40 @@ +// LoopFollow +// AppearanceMode.swift + +import SwiftUI + +extension Notification.Name { + static let appearanceDidChange = Notification.Name("appearanceDidChange") +} + +enum AppearanceMode: String, CaseIterable, Codable { + case system + case light + case dark + + var displayName: String { + switch self { + case .system: return "System" + case .light: return "Light" + case .dark: return "Dark" + } + } + + /// Returns the ColorScheme for SwiftUI's preferredColorScheme modifier + var colorScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } + + /// Returns the UIUserInterfaceStyle for UIKit views + var userInterfaceStyle: UIUserInterfaceStyle { + switch self { + case .system: return .unspecified + case .light: return .light + case .dark: return .dark + } + } +} diff --git a/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift b/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift index 20dd2acee..672bb9991 100644 --- a/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift +++ b/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift @@ -16,11 +16,10 @@ struct SimpleQRCodeScannerView: UIViewControllerRepresentable { let navController = UINavigationController(rootViewController: scannerVC) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - scannerVC.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + scannerVC.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style return navController } diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index 7d5fb739b..400d3ed77 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -42,7 +42,7 @@ struct InfoDisplaySettingsView: View { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Information Display Settings", displayMode: .inline) } } diff --git a/LoopFollow/Log/LogView.swift b/LoopFollow/Log/LogView.swift index bc430fc76..a99088ac4 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -36,7 +36,7 @@ struct LogView: View { viewModel.loadLogEntries() } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Today's Logs", displayMode: .inline) } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 794f26125..7c08e756f 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -18,7 +18,7 @@ struct NightscoutSettingsView: View { viewModel.dismiss() } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Nightscout Settings", displayMode: .inline) } diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index 1187ddee2..56e317da3 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -6,19 +6,42 @@ import SwiftUI import UIKit class RemoteViewController: UIViewController { - private var cancellable: AnyCancellable? + private var cancellables = Set() private var hostingController: UIHostingController? override func viewDidLoad() { super.viewDidLoad() - cancellable = Storage.shared.device.$value + // Apply initial appearance + overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + Storage.shared.device.$value .removeDuplicates() .sink { [weak self] _ in DispatchQueue.main.async { self?.updateView() } } + .store(in: &cancellables) + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.overrideUserInterfaceStyle = mode.userInterfaceStyle + self?.hostingController?.overrideUserInterfaceStyle = mode.userInterfaceStyle + } + .store(in: &cancellables) + + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + self?.overrideUserInterfaceStyle = style + self?.hostingController?.overrideUserInterfaceStyle = style + } + .store(in: &cancellables) } private func updateView() { @@ -89,7 +112,15 @@ class RemoteViewController: UIViewController { updateView() } - deinit { - cancellable?.cancel() + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + overrideUserInterfaceStyle = style + hostingController?.overrideUserInterfaceStyle = style + } } } diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index ae4f6dd7a..1c1066197 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -373,7 +373,7 @@ struct RemoteSettingsView: View { // The sheet will be shown automatically due to the binding } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationTitle("Remote Settings") .navigationBarTitleDisplayMode(.inline) } diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 12abd564f..1873aabf5 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -27,7 +27,7 @@ struct AdvancedSettingsView: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Advanced Settings", displayMode: .inline) } } diff --git a/LoopFollow/Settings/CalendarSettingsView.swift b/LoopFollow/Settings/CalendarSettingsView.swift index 0d6ed840a..704ef5e2f 100644 --- a/LoopFollow/Settings/CalendarSettingsView.swift +++ b/LoopFollow/Settings/CalendarSettingsView.swift @@ -74,7 +74,7 @@ struct CalendarSettingsView: View { await requestCalendarAccessAndLoad() } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Calendar", displayMode: .inline) } diff --git a/LoopFollow/Settings/ContactSettingsView.swift b/LoopFollow/Settings/ContactSettingsView.swift index e91da2dfd..018a49a9f 100644 --- a/LoopFollow/Settings/ContactSettingsView.swift +++ b/LoopFollow/Settings/ContactSettingsView.swift @@ -79,7 +79,7 @@ struct ContactSettingsView: View { Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK"))) } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Contact", displayMode: .inline) } diff --git a/LoopFollow/Settings/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift index f3f2eb30b..c93b31855 100644 --- a/LoopFollow/Settings/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -37,7 +37,7 @@ struct DexcomSettingsView: View { importSection } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Dexcom Settings", displayMode: .inline) } diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index df63e26ff..e94a9040e 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -6,7 +6,7 @@ import SwiftUI struct GeneralSettingsView: View { @ObservedObject var colorBGText = Storage.shared.colorBGText @ObservedObject var appBadge = Storage.shared.appBadge - @ObservedObject var forceDarkMode = Storage.shared.forceDarkMode + @ObservedObject var appearanceMode = Storage.shared.appearanceMode @ObservedObject var showStats = Storage.shared.showStats @ObservedObject var useIFCC = Storage.shared.useIFCC @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph @@ -36,7 +36,11 @@ struct GeneralSettingsView: View { } Section("Display") { - Toggle("Force Dark Mode (restart app)", isOn: $forceDarkMode.value) + Picker("Appearance", selection: $appearanceMode.value) { + ForEach(AppearanceMode.allCases, id: \.self) { mode in + Text(mode.displayName).tag(mode) + } + } Toggle("Display Stats", isOn: $showStats.value) Toggle("Use IFCC A1C", isOn: $useIFCC.value) Toggle("Display Small Graph", isOn: $showSmallGraph.value) @@ -115,7 +119,7 @@ struct GeneralSettingsView: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("General Settings", displayMode: .inline) } } diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index 441600ac1..4ebc1896a 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -132,7 +132,7 @@ struct GraphSettingsView: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) .navigationBarTitle("Graph Settings", displayMode: .inline) } diff --git a/LoopFollow/Settings/TabCustomizationModal.swift b/LoopFollow/Settings/TabCustomizationModal.swift index 2f9ca4a30..572b3a1e0 100644 --- a/LoopFollow/Settings/TabCustomizationModal.swift +++ b/LoopFollow/Settings/TabCustomizationModal.swift @@ -103,7 +103,7 @@ struct TabCustomizationModal: View { } } } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } private func checkForChanges() { diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index e558d3cbf..ea63d8e0f 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -7,6 +7,7 @@ import UIKit class SnoozerViewController: UIViewController { private var hostingController: UIHostingController? + private var cancellables = Set() @State private var snoozeMinutes = 15 @@ -18,6 +19,26 @@ class SnoozerViewController: UIViewController { let hosting = UIHostingController(rootView: snoozerView) hostingController = hosting + + // Apply initial appearance + hosting.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.hostingController?.overrideUserInterfaceStyle = mode.userInterfaceStyle + } + .store(in: &cancellables) + + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.hostingController?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + .store(in: &cancellables) + addChild(hosting) view.addSubview(hosting.view) hosting.view.translatesAutoresizingMaskIntoConstraints = false @@ -31,4 +52,14 @@ class SnoozerViewController: UIViewController { hosting.didMove(toParent: self) } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + hostingController?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + } } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 17ab82fb1..552dc0e49 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -59,7 +59,8 @@ extension Storage { let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) if legacyForceDarkMode.exists { - Storage.shared.forceDarkMode.value = legacyForceDarkMode.value + // Migrate from Bool to AppearanceMode: true -> .dark, false -> .system + Storage.shared.appearanceMode.value = legacyForceDarkMode.value ? .dark : .system legacyForceDarkMode.setNil(key: "forceDarkMode") } @@ -152,7 +153,7 @@ extension Storage { // ── General (done earlier, but safe to repeat) ── move(UserDefaultsValue(key: "colorBGText", default: true), into: Storage.shared.colorBGText) move(UserDefaultsValue(key: "appBadge", default: true), into: appBadge) - move(UserDefaultsValue(key: "forceDarkMode", default: false), into: forceDarkMode) + // Note: forceDarkMode migration to appearanceMode is handled earlier in migrateGeneralSettings() move(UserDefaultsValue(key: "showStats", default: true), into: showStats) move(UserDefaultsValue(key: "useIFCC", default: false), into: useIFCC) move(UserDefaultsValue(key: "showSmallGraph", default: true), into: showSmallGraph) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 2924d9541..bb5034f18 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -60,7 +60,7 @@ class Storage { // General Settings [BEGIN] var appBadge = StorageValue(key: "appBadge", defaultValue: true) var colorBGText = StorageValue(key: "colorBGText", defaultValue: true) - var forceDarkMode = StorageValue(key: "forceDarkMode", defaultValue: true) + var appearanceMode = StorageValue(key: "appearanceMode", defaultValue: .system) var showStats = StorageValue(key: "showStats", defaultValue: true) var useIFCC = StorageValue(key: "useIFCC", defaultValue: false) var showSmallGraph = StorageValue(key: "showSmallGraph", defaultValue: true) diff --git a/LoopFollow/ViewControllers/AlarmViewController.swift b/LoopFollow/ViewControllers/AlarmViewController.swift index 2375b791b..1b3c4d60b 100644 --- a/LoopFollow/ViewControllers/AlarmViewController.swift +++ b/LoopFollow/ViewControllers/AlarmViewController.swift @@ -1,16 +1,38 @@ // LoopFollow // AlarmViewController.swift +import Combine import SwiftUI import UIKit class AlarmViewController: UIViewController { + private var hostingController: UIHostingController! + private var cancellables = Set() + override func viewDidLoad() { super.viewDidLoad() let alarmsView = AlarmsContainerView() + hostingController = UIHostingController(rootView: alarmsView) + + // Apply initial appearance + hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.hostingController.overrideUserInterfaceStyle = mode.userInterfaceStyle + } + .store(in: &cancellables) - let hostingController = UIHostingController(rootView: alarmsView) + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + .store(in: &cancellables) addChild(hostingController) view.addSubview(hostingController.view) @@ -25,4 +47,14 @@ class AlarmViewController: UIViewController { hostingController.didMove(toParent: self) } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 4f4cd89c7..9ccbaa5bf 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -173,10 +173,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele BGChart.delegate = self BGChartFull.delegate = self - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - tabBarController?.overrideUserInterfaceStyle = .dark - } + // Apply initial appearance mode + updateAppearance(Storage.shared.appearanceMode.value) // Trigger foreground and background functions let notificationCenter = NotificationCenter.default @@ -256,6 +254,14 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .store(in: &cancellables) + // Update appearance when setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.updateAppearance(mode) + } + .store(in: &cancellables) + Storage.shared.showStats.$value .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -895,6 +901,43 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } + func updateAppearance(_ mode: AppearanceMode) { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first else { return } + + let style: UIUserInterfaceStyle + switch mode { + case .light: + style = .light + case .dark: + style = .dark + case .system: + // Use .unspecified to follow system + style = .unspecified + } + + // Update this view controller + overrideUserInterfaceStyle = style + + // Update the tab bar controller (affects all tabs) + tabBarController?.overrideUserInterfaceStyle = style + + // Update the window (affects the entire app including modals) + window.overrideUserInterfaceStyle = style + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + // When system appearance changes and we're in "System" mode, notify all observers + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + // Post notification so other view controllers can update if needed + NotificationCenter.default.post(name: .appearanceDidChange, object: nil) + } + } + func bgDirectionGraphic(_ value: String) -> String { let // graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"] graphics: [String: String] = ["Flat": "→", "DoubleUp": "↑↑", "SingleUp": "↑", "FortyFiveUp": "↗", "FortyFiveDown": "↘︎", "SingleDown": "↓", "DoubleDown": "↓↓", "None": "-", "NONE": "-", "NOT COMPUTABLE": "-", "RATE OUT OF RANGE": "-", "": "-"] @@ -1183,11 +1226,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let hostingController = UIHostingController(rootView: nightscoutSettingsView) let navController = UINavigationController(rootViewController: hostingController) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + hostingController.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a Done button hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -1205,11 +1247,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let hostingController = UIHostingController(rootView: dexcomSettingsView) let navController = UINavigationController(rootViewController: hostingController) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + hostingController.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a Done button hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem( diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift index 5c01e04c5..c714d1bc0 100644 --- a/LoopFollow/ViewControllers/MoreMenuViewController.swift +++ b/LoopFollow/ViewControllers/MoreMenuViewController.swift @@ -1,11 +1,13 @@ // LoopFollow // MoreMenuViewController.swift +import Combine import SwiftUI import UIKit class MoreMenuViewController: UIViewController { private var tableView: UITableView! + private var cancellables = Set() struct MenuItem { let title: String @@ -21,10 +23,24 @@ class MoreMenuViewController: UIViewController { title = "More" view.backgroundColor = .systemBackground - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.overrideUserInterfaceStyle = mode.userInterfaceStyle + } + .store(in: &cancellables) + + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + .store(in: &cancellables) setupTableView() updateMenuItems() @@ -37,6 +53,16 @@ class MoreMenuViewController: UIViewController { Observable.shared.settingsPath.set(NavigationPath()) } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + } + private func setupTableView() { tableView = UITableView(frame: view.bounds, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -102,11 +128,10 @@ class MoreMenuViewController: UIViewController { let settingsVC = UIHostingController(rootView: SettingsMenuView()) let navController = UINavigationController(rootViewController: settingsVC) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - settingsVC.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + settingsVC.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a close button settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -124,11 +149,10 @@ class MoreMenuViewController: UIViewController { let alarmsVC = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") let navController = UINavigationController(rootViewController: alarmsVC) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - alarmsVC.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + alarmsVC.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a close button alarmsVC.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -146,11 +170,10 @@ class MoreMenuViewController: UIViewController { let remoteVC = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") let navController = UINavigationController(rootViewController: remoteVC) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - remoteVC.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + remoteVC.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a close button remoteVC.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -168,11 +191,10 @@ class MoreMenuViewController: UIViewController { let nightscoutVC = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController") let navController = UINavigationController(rootViewController: nightscoutVC) - // Apply dark mode if needed - if Storage.shared.forceDarkMode.value { - nightscoutVC.overrideUserInterfaceStyle = .dark - navController.overrideUserInterfaceStyle = .dark - } + // Apply appearance mode + let style = Storage.shared.appearanceMode.value.userInterfaceStyle + nightscoutVC.overrideUserInterfaceStyle = style + navController.overrideUserInterfaceStyle = style // Add a close button nightscoutVC.navigationItem.rightBarButtonItem = UIBarButtonItem( diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 532025107..cecb29c68 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -1,17 +1,33 @@ // LoopFollow // NightScoutViewController.swift +import Combine import UIKit import WebKit class NightscoutViewController: UIViewController { @IBOutlet var webView: WKWebView! + private var cancellables = Set() override func viewDidLoad() { super.viewDidLoad() - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } + overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.overrideUserInterfaceStyle = mode.userInterfaceStyle + } + .store(in: &cancellables) + + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + .store(in: &cancellables) var url = Storage.shared.url.value let token = Storage.shared.token.value @@ -41,6 +57,16 @@ class NightscoutViewController: UIViewController { sender.endRefreshing() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } + } + func clearWebCache() { let dataStore = WKWebsiteDataStore.default() let cacheTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]) diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 6c168caa9..b27e32171 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -1,6 +1,7 @@ // LoopFollow // SettingsViewController.swift +import Combine import SwiftUI import UIKit @@ -8,6 +9,7 @@ final class SettingsViewController: UIViewController { // MARK: Stored properties private var host: UIHostingController! + private var cancellables = Set() // MARK: Life-cycle @@ -17,10 +19,24 @@ final class SettingsViewController: UIViewController { // Build SwiftUI menu host = UIHostingController(rootView: SettingsMenuView()) - // Dark-mode override - if Storage.shared.forceDarkMode.value { - host.overrideUserInterfaceStyle = .dark - } + // Appearance mode override + host.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + + // Listen for appearance setting changes + Storage.shared.appearanceMode.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] mode in + self?.updateAppearance(mode) + } + .store(in: &cancellables) + + // Listen for system appearance changes (when in System mode) + NotificationCenter.default.publisher(for: .appearanceDidChange) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateAppearance(Storage.shared.appearanceMode.value) + } + .store(in: &cancellables) // Embed addChild(host) @@ -40,4 +56,18 @@ final class SettingsViewController: UIViewController { Observable.shared.settingsPath.set(NavigationPath()) } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if Storage.shared.appearanceMode.value == .system, + previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle + { + updateAppearance(.system) + } + } + + private func updateAppearance(_ mode: AppearanceMode) { + host.overrideUserInterfaceStyle = mode.userInterfaceStyle + } }