Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
8
Help us understand the problem. What is going on with this article?
@hituziando

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を監視する方法もありますが、公式ドキュメントに記載がない(?)ため、積極的に使っていいものなのか悩ましいです。

プログラムでライト/ダーク・モードを切り替える

最後に、プログラムでライト/ダーク・モードを切り替える方法を紹介します。

// ダークモードにする
NSApp.appearance = NSAppearance(named: .darkAqua)
// ライドモードにする
NSApp.appearance = NSAppearance(named: .aqua)
// プログラムでの設定を解除する -> Macの設定に従う
NSApp.appearance = nil

参考

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

8
Help us understand the problem. What is going on with this article?
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
hituziando
Android/iOS/macOS app developer, Front-end engineer, Rails engineer

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
8
Help us understand the problem. What is going on with this article?