feat: in-app dark mode toggle (System / Light / Dark)
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

- 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) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 20:22:35 -05:00
parent da44ee8b73
commit 028e308588
4 changed files with 63 additions and 0 deletions

View File

@@ -12,6 +12,7 @@
A10003 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10003 /* Config.swift */; }; A10003 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10003 /* Config.swift */; };
A10004 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10004 /* APIClient.swift */; }; A10004 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10004 /* APIClient.swift */; };
A10005 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10005 /* AuthManager.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 */; }; A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; };
A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; }; A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; };
A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.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 = "<group>"; }; B10003 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
B10004 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; }; B10004 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
B10005 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; }; B10005 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
B10050 /* AppearanceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceManager.swift; sourceTree = "<group>"; };
B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; }; B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
@@ -137,6 +139,7 @@
children = ( children = (
B10004 /* APIClient.swift */, B10004 /* APIClient.swift */,
B10005 /* AuthManager.swift */, B10005 /* AuthManager.swift */,
B10050 /* AppearanceManager.swift */,
); );
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -411,6 +414,7 @@
A10003 /* Config.swift in Sources */, A10003 /* Config.swift in Sources */,
A10004 /* APIClient.swift in Sources */, A10004 /* APIClient.swift in Sources */,
A10005 /* AuthManager.swift in Sources */, A10005 /* AuthManager.swift in Sources */,
A10050 /* AppearanceManager.swift in Sources */,
A10006 /* LoginView.swift in Sources */, A10006 /* LoginView.swift in Sources */,
A10007 /* HomeView.swift in Sources */, A10007 /* HomeView.swift in Sources */,
A10008 /* HomeViewModel.swift in Sources */, A10008 /* HomeViewModel.swift in Sources */,

View File

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

View File

@@ -3,6 +3,7 @@ import PhotosUI
struct HomeView: View { struct HomeView: View {
@Environment(AuthManager.self) private var auth @Environment(AuthManager.self) private var auth
@Environment(AppearanceManager.self) private var appearance
@State private var vm = HomeViewModel() @State private var vm = HomeViewModel()
@State private var ringAnimated = false @State private var ringAnimated = false
@Binding var selectedTab: Int @Binding var selectedTab: Int
@@ -50,6 +51,18 @@ struct HomeView: View {
} }
} }
Divider() 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) { Button(role: .destructive) {
Task { await auth.logout() } Task { await auth.logout() }
} label: { } label: {

View File

@@ -3,11 +3,14 @@ import SwiftUI
@main @main
struct PlatformApp: App { struct PlatformApp: App {
@State private var authManager = AuthManager() @State private var authManager = AuthManager()
@State private var appearance = AppearanceManager()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
.environment(authManager) .environment(authManager)
.environment(appearance)
.preferredColorScheme(appearance.mode.colorScheme)
} }
} }
} }