The standard way to check for a change between Dark and Light modes on iOS is with a view-level delegation function:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    switch self.traitCollection.userInterfaceStyle {
    case .dark:
        print("dark")
    case .light:
        print("light")
    case .unspecified:
        print("unspecified")
    @unknown default:
        fatalError()
    }
}

This works well, but in an app with 100's of view controllers, adding this call to every controller is a pain in the butt and is messy. What I'd like to do is observe, via NotificationCenter, changes between light and dark on the AppDelegate level. That way I can make one global theme-changing function that applies to all views.

I tried the following in AppDelegate.swift but the daytimeDidChange never gets called when changing between dark/light:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    NotificationCenter.default.addObserver(self, selector: #selector(daytimeDidChange), name: nil, object: UIScreen.main.traitCollection)

}

@objc func daytimeDidChange() {
     if UITraitCollection.current.userInterfaceStyle == .dark {
         //Dark
         print("dark")
     }
     else {
         //Light
         print("light")
     }
}

Any ideas on how to set up the Notification observer properly?

1

There are 1 best solutions below

0
On BEST ANSWER

Let's see what we have. I write a full answer, so if you want, skip the first paragraph.

The standard solution - as you mentioned - would be to implement this method in every (of at least in some) UIView / UIViewController:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    print("isDark: \(UITraitCollection.current.userInterfaceStyle == .dark)")
}

If that doesn't fit your needs, then here is another solution, which - as you want - works with notifications:

First, define a custom NSNotification.Name and a custom UIWindow implementation like this:

let traitCollectionDidChangeNotification = NSNotification.Name("traitCollectionDidChange")

final class MyWindow: UIWindow {
    private var userInterfaceStyle = UITraitCollection.current.userInterfaceStyle

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        let currentUserInterfaceStyle = UITraitCollection.current.userInterfaceStyle
        if currentUserInterfaceStyle != userInterfaceStyle {
            userInterfaceStyle = currentUserInterfaceStyle
            NotificationCenter.default.post(name: traitCollectionDidChangeNotification, object: self)
        }
    }
}

You can work with Strings in the implementation if you don't want to define a global variable (although it can be outsourced to a Config file or whatever): just replace NotificationCenter.default.post(name: traitCollectionDidChangeNotification, object: self) with NotificationCenter.default.post(name: NSNotification.Name("traitCollectionDidChange"), object: self).

Then write this (if you use AppDelegate like me, then in the AppDelegate, if SceneDelegate then there. I use AppDelegate, so the example fits that, but it works with SceneDelegate too):

@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = MyWindow(frame: UIScreen.main.bounds)

        // instantiate your root UIViewController. Of course you can do it anyhow, as you want, don't have to use Storyboards etc.
        window?.rootViewController = UIStoryboard(name: "xyz", bundle: nil).instantiateInitialViewController()!

        window?.makeKeyAndVisible()

        return true
    }
    
    // ...
}

From now on you receive notifications if the userInterfaceStyle changed. In the custom MyWindow implementation you can check other traits, and you don't have to check that it really changed (the private userInterfaceStyle property).

To get these notifications, write it literally anywhere (not only in UIView or UIViewController):

NotificationCenter.default.addObserver(forName: traitCollectionDidChangeNotification, object: nil, queue: .main) { _ in
    print("isDark: \(UITraitCollection.current.userInterfaceStyle == .dark)")
    // Do your things...
}

For example if you want to use in a UIViewController:

final class MyViewController: UIViewController {
    
    private var observer: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        observer = NotificationCenter.default.addObserver(forName: traitCollectionDidChangeNotification, object: nil, queue: .main) { _ in
            print("isDark: \(UITraitCollection.current.userInterfaceStyle == .dark)")
        }
    }

    deinit {
        if let observer = observer {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

(I know that there is a debate about the removal of the notification observers - as of this explanation I think the best to remove, but if your taste doesn't like that, just ignore the observer.)

I tested it on a real device (iPhone SE 2), it worked. If you have any question, feel free to ask! I hope it helps. :)