From 028e30858820dac3b8c563f229afe3abf7cf3c0b Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Fri, 3 Apr 2026 20:22:35 -0500 Subject: [PATCH] feat: in-app dark mode toggle (System / Light / Dark) - AppearanceManager with UserDefaults persistence - Three modes: System (follows iOS), Light, Dark - Toggle in Home screen profile menu under "Appearance" - Applied via .preferredColorScheme at app root - Persists across app launches Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Platform.xcodeproj/project.pbxproj | 4 ++ .../Platform/Core/AppearanceManager.swift | 43 +++++++++++++++++++ .../Platform/Features/Home/HomeView.swift | 13 ++++++ ios/Platform/Platform/PlatformApp.swift | 3 ++ 4 files changed, 63 insertions(+) create mode 100644 ios/Platform/Platform/Core/AppearanceManager.swift diff --git a/ios/Platform/Platform.xcodeproj/project.pbxproj b/ios/Platform/Platform.xcodeproj/project.pbxproj index 9280126..c522120 100644 --- a/ios/Platform/Platform.xcodeproj/project.pbxproj +++ b/ios/Platform/Platform.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ A10003 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10003 /* Config.swift */; }; A10004 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10004 /* APIClient.swift */; }; A10005 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10005 /* AuthManager.swift */; }; + A10050 /* AppearanceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10050 /* AppearanceManager.swift */; }; A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; }; A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; }; A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.swift */; }; @@ -57,6 +58,7 @@ B10003 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; B10004 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; B10005 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; + B10050 /* AppearanceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceManager.swift; sourceTree = ""; }; B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -137,6 +139,7 @@ children = ( B10004 /* APIClient.swift */, B10005 /* AuthManager.swift */, + B10050 /* AppearanceManager.swift */, ); path = Core; sourceTree = ""; @@ -411,6 +414,7 @@ A10003 /* Config.swift in Sources */, A10004 /* APIClient.swift in Sources */, A10005 /* AuthManager.swift in Sources */, + A10050 /* AppearanceManager.swift in Sources */, A10006 /* LoginView.swift in Sources */, A10007 /* HomeView.swift in Sources */, A10008 /* HomeViewModel.swift in Sources */, diff --git a/ios/Platform/Platform/Core/AppearanceManager.swift b/ios/Platform/Platform/Core/AppearanceManager.swift new file mode 100644 index 0000000..d4d76ae --- /dev/null +++ b/ios/Platform/Platform/Core/AppearanceManager.swift @@ -0,0 +1,43 @@ +import SwiftUI + +@Observable +final class AppearanceManager { + enum Mode: String, CaseIterable { + case system, light, dark + + var label: String { + switch self { + case .system: return "System" + case .light: return "Light" + case .dark: return "Dark" + } + } + + var icon: String { + switch self { + case .system: return "circle.lefthalf.filled" + case .light: return "sun.max.fill" + case .dark: return "moon.fill" + } + } + + var colorScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } + } + + var mode: Mode { + didSet { + UserDefaults.standard.set(mode.rawValue, forKey: "appearance_mode") + } + } + + init() { + let saved = UserDefaults.standard.string(forKey: "appearance_mode") ?? "system" + mode = Mode(rawValue: saved) ?? .system + } +} diff --git a/ios/Platform/Platform/Features/Home/HomeView.swift b/ios/Platform/Platform/Features/Home/HomeView.swift index ab5cb2a..5c316dc 100644 --- a/ios/Platform/Platform/Features/Home/HomeView.swift +++ b/ios/Platform/Platform/Features/Home/HomeView.swift @@ -3,6 +3,7 @@ import PhotosUI struct HomeView: View { @Environment(AuthManager.self) private var auth + @Environment(AppearanceManager.self) private var appearance @State private var vm = HomeViewModel() @State private var ringAnimated = false @Binding var selectedTab: Int @@ -50,6 +51,18 @@ struct HomeView: View { } } Divider() + Menu { + ForEach(AppearanceManager.Mode.allCases, id: \.self) { mode in + Button { + appearance.mode = mode + } label: { + Label(mode.label, systemImage: mode.icon) + } + } + } label: { + Label("Appearance", systemImage: appearance.mode.icon) + } + Divider() Button(role: .destructive) { Task { await auth.logout() } } label: { diff --git a/ios/Platform/Platform/PlatformApp.swift b/ios/Platform/Platform/PlatformApp.swift index 8f3709b..2decd0b 100644 --- a/ios/Platform/Platform/PlatformApp.swift +++ b/ios/Platform/Platform/PlatformApp.swift @@ -3,11 +3,14 @@ import SwiftUI @main struct PlatformApp: App { @State private var authManager = AuthManager() + @State private var appearance = AppearanceManager() var body: some Scene { WindowGroup { ContentView() .environment(authManager) + .environment(appearance) + .preferredColorScheme(appearance.mode.colorScheme) } } }