Help us understand the problem. What is going on with this article?

macOSアプリのダークモード対応

はじめに

macOSでは10.14(2018年リリース)からダークモードが追加されています。

macOS 10.14以降、アプリで独自定義のカラーを使う場合、ライトモード用とダークモード用のカラーを用意しておく必要があります。ライト/ダーク用のカラーはカラーアセットを利用して用意できます。

スクリーンショット 2019-10-20 19.50.47.png

カラーアセットから以下のコードで外観モードにあわせたカラーオブジェクトを生成できます。

let color = NSColor(named: NSColor.Name("MyColor"))

しかし、この方法ではモードを切り替え時、起動中のアプリでは自動的にカラーが変更されません。macOS 10.15ではNSColorにinit(name:dynamicProvider:)イニシャライザが追加されており、ドキュメントに説明は無いですが、察するにこれを使えば動的に変更できるものと思います。では、macOS 10.14ではどうするか?

macOS 10.14でダークモードに対応する

macOS 10.14では、独自定義のカラーをViewに設定していた場合、モードの変更を検知し、カラーを設定し直すコードを書く必要があります。

ダークモードか判定する

ダークモードか判定するisDarkModeコンピューテッドプロパティをNSApplicationに生やします。

extension NSApplication {
    public var isDarkMode: Bool {
        if #available(OSX 10.14, *) {
            let name = effectiveAppearance.name
            return name == .darkAqua
        }
        else {
            return false
        }
    }
}

外観モードにあわせてカラーオブジェクトを返す

ライト/ダークモード用のカラーオブジェクトを返す適当なメソッドを実装します。

struct CustomColors {
    static var background: NSColor {
        if NSApplication.shared.isDarkMode {
            return NSColor(red: 34.0 / 255.0, green: 34.0 / 255.0, blue: 34.0 / 255.0, alpha: 1.0)
        }
        return .white    // for Light Mode
    }
}

外観モードの変更を検知する

外観モードが変更されると、NSViewのviewDidChangeEffectiveAppearanceメソッドが呼ばれます。このメソッドをオーバライドし、カラーオブジェクトを再設定するようにします。

class CustomView: NSView {

    @available(OSX 10.14, *)
    override func viewDidChangeEffectiveAppearance() {
        layer?.backgroundColor = CustomColors.background.cgColor
    }
}

これで動的にカラーを変更することができます。ただ、このやり方は各ビューで継承クラスを作らないといけないため、使い勝手が悪いときがあります。

そこで僕は以下のようなクラスを作って監視するようにしています。

class AppearanceMonitor: NSView {

    public static let appearanceChangedNotification = NSNotification.Name("AppearanceChangedNotification")
    public static let shared                        = AppearanceMonitor(frame: .zero)

    public var isMonitoring: Bool {
        superview != nil
    }

    override func viewDidMoveToSuperview() {
        guard let _ = superview else {
            print("[\(self.className)] Monitoring stopped.")
            return
        }
        print("[\(self.className)] Monitoring started.")
    }

    @available(OSX 10.14, *)
    override func viewDidChangeEffectiveAppearance() {
        NotificationCenter.default.post(name: AppearanceMonitor.appearanceChangedNotification, object: nil, userInfo: nil)
    }

    public func start(with viewController: NSViewController) {
        if !isMonitoring {
            viewController.view.addSubview(self)
        }
    }

    public func stop() {
        if isMonitoring {
            removeFromSuperview()
        }
    }
}
class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // 監視用のビューを貼る(frame=.zeroなので見た目には影響がない)
        AppearanceMonitor.shared.start(with: self)
    }

    override func viewWillAppear() {
        super.viewWillAppear()

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(appearanceChanged),
                                               name: AppearanceMonitor.appearanceChangedNotification,
                                               object: nil)
    }

    override func viewWillDisappear() {
        super.viewWillDisappear()

        NotificationCenter.default.removeObserver(self,
                                                  name: AppearanceMonitor.appearanceChangedNotification,
                                                  object: nil)
    }

    @objc func appearanceChanged() {
        // ライト/ダーク・モード切り替え時に呼ばれるのでUIを更新する
    }
}

他にAppleInterfaceThemeChangedNotificationを監視する方法もありますが、公式ドキュメントに記載がない(?)ため、積極的に使っていいものか悩ましい…。

参考

Supporting Dark Mode in Your Interface
https://developer.apple.com/documentation/xcode/supporting_dark_mode_in_your_interface

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした