Change white and dark mode in SwiftUI App and persist it
Asked Answered
L

2

5

I want my app to support both light and dark themes, where users can change the theme to their preference or keep it as the default system theme. When users switch the theme to dark or light mode, the app should remember their choice and apply the same theme the next time they open the app. And I don't want to change it for every single view instead I want it for whole app. Now its changing the theme but when I reopened the app it sets to default.

I changing the theme like this :

struct Settings1: View {
    @Environment(\.colorScheme) private var colorScheme
    var body: some View {
            VStack(alignment: .leading){
                Text("Select Mode")
                    .font(.custom("Inter-Medium", size: 22))
                Spacer()
                Button {
                    changeTheme(to: .dark)
                } label: {
                    HStack{
                        ZStack{
                            Circle()
                                .fill(colorScheme == .dark ? Color.green:Color.gray)
                                .frame(width:32 ,height: 32)
                            Circle()
                                .fill(Color.black)
                                .frame(width:30 ,height: 30)
                        }
                        Text("Dark Mode")
                            .font(.custom("Inter-Medium", size: 19))
                            .foregroundColor(Color("dayNightText"))
                            .padding(.leading)
                        Spacer()
                    }
                }
                Button {
                    changeTheme(to: .light)
                } label: {
                    HStack{
                        ZStack{
                            Circle()
                                .fill(colorScheme == .light ? Color.green:Color.gray)
                                .frame(width:32 ,height: 32)
                            Circle()
                                .fill(Color.white)
                                .frame(width:30 ,height: 30)
                        }
                        Text("White Mode")
                            .font(.custom("Inter-Medium", size: 19))
                            .foregroundColor(Color("dayNightText"))
                            .padding(.leading)
                        Spacer()
                    }
                }
                Button {
                    changeTheme(to: .device)
                } label: {
                    HStack{
                        ZStack{
                            Circle()
                                .fill(colorScheme == .light ? Color.green:Color.gray)
                                .frame(width:32 ,height: 32)
                            Circle()
                                .fill(Color.white)
                                .frame(width:30 ,height: 30)
                        }
                        Text("System Default")
                            .font(.custom("Inter-Medium", size: 19))
                            .foregroundColor(Color("dayNightText"))
                            .padding(.leading)
                        Spacer()
                    }
                }
                Spacer()
            }
            .frame(width: 300, height: 300)
            .background(Color.gray.ignoresSafeArea())
            .cornerRadius(30)
    }
    func changeTheme(to theme: Theme) {
        UserDefaults.standard.theme = theme
        UIApplication.shared.windows.first?.overrideUserInterfaceStyle  = theme.userInterfaceStyle
    }
}
struct Settings1_Previews: PreviewProvider {
    static var previews: some View {
        Settings1()
    }
}


enum Theme: Int {
  case device
  case light
  case dark
}
extension Theme {
  var userInterfaceStyle: UIUserInterfaceStyle {
    switch self {
      case .device:
        return .unspecified
      case .light:
        return .light
      case .dark:
        return .dark
    }
  }
}

extension UserDefaults {
  var theme: Theme {
    get {
      register(defaults: [#function: Theme.device.rawValue])
      return Theme(rawValue: integer(forKey: #function)) ?? .device
    }
    set {
      set(newValue.rawValue, forKey: #function)
    }
  }
}


and then on re launch trying to recover the previous one but its not working

import SwiftUI

@main
struct TestApp: App {
    init(){
        let savedTheme = UserDefaults.standard.theme
        UIApplication.shared.windows.first?.overrideUserInterfaceStyle = savedTheme.userInterfaceStyle
    }
    var body: some Scene {
        WindowGroup {
            NavigationView{
                Settings1()
            }
        }
    }
}
Lupe answered 20/7, 2023 at 12:45 Comment(1)
You can set the colorScheme for the entire view hierarchy simply by setting .preferredColorScheme(). In order to have the third option device, you can use a view modifier to alternately inject the .preferredColorScheme() into the top view in the @ main file. UIApplication.shared.windows.first is deprecated. Even on iPhone, it is possible to have more than 1 window, so using first may not cut it.Aquileia
D
9

You can incorporate .preferredColorScheme to set the colorScheme on launch.

@main
struct StackOverflowDebuggingApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                Settings1()
            } .preferredColorScheme(UserDefaults.standard.theme.colorScheme)
        }
    }
}

Then add a colorScheme extension to Theme exactly like how you already did with userInterfaceStyle

extension Theme {
  var userInterfaceStyle: UIUserInterfaceStyle {
    switch self {
      case .device:
        return .unspecified
      case .light:
        return .light
      case .dark:
        return .dark
    }
  }
  var colorScheme: ColorScheme? {
    switch self {
      case .device:
        return nil
      case .light:
        return .light
      case .dark:
        return .dark
    }
  }
}

To be able to override the preferredColorScheme though update your changeTheme function to override the rootViewController like so

func changeTheme(to theme: Theme) {
    UserDefaults.standard.theme = theme
    UIApplication.shared.windows.first?.rootViewController?.overrideUserInterfaceStyle = theme.userInterfaceStyle
}

Ran fine for me in Xcode 15 Beta on an iOS 17 simulator, not sure what version you're on though.

Deleterious answered 24/7, 2023 at 7:44 Comment(0)
D
0

With iOS 17 and Swift 5.10, you can persist UIUserInterfaceStyle with AppStorage. You'll then need to use UIApplicationDelegateAdaptor in order to access the app's window and update its overrideUserInterfaceStyle property when the persisted user interface style is changed.

AppDelegate.swift

import SwiftUI

class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
    ) -> Bool {
        return true
    }

    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
        if connectingSceneSession.role == .windowApplication {
            configuration.delegateClass = SceneDelegate.self
        }
        return configuration
    }
}

SceneDelegate.swift

import SwiftUI

class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = scene as? UIWindowScene else { return }
        self.window = windowScene.keyWindow
    }
}

UIUserInterfaceStyle+extensions.swift

import UIKit

extension UIUserInterfaceStyle {
    var name: String {
        switch self {
        case .unspecified:
            "System"
        case .light:
            "Light"
        case .dark:
            "Dark"
        @unknown default:
            fatalError("Unknown User Interface Style.")
        }
    }
}

extension UIUserInterfaceStyle: CaseIterable { // Use @retroactive CaseIterable for projects built with Swift 6.
    public static var allCases: [UIUserInterfaceStyle] = [.unspecified, .light, .dark]
}

MyDemoApp.swift

import SwiftUI

@main
struct MyDemoApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    @AppStorage("UserInterfaceStyleKey") var userInterfaceStyle = UIUserInterfaceStyle.unspecified
    @EnvironmentObject var sceneDelegate: SceneDelegate

    var body: some View {
        Picker("Choose a User Interface Style", selection: $userInterfaceStyle) {
            ForEach(UIUserInterfaceStyle.allCases, id: \.self) { style in
                Text(style.name)
            }
        }
        .onChange(of: userInterfaceStyle, initial: true) { _, newUserInterfaceStyle in
            sceneDelegate.window?.overrideUserInterfaceStyle = newUserInterfaceStyle
        }
    }
}
Dextrocular answered 14/7 at 12:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.