🐰

AppKitやCore Animationのカラーに現在のアピアランス(ダークモード等)を反映する

に公開

extension

macOS 11 BigSur以降のシステムではNSAppearanceのメソッドperformAsCurrentDrawingAppearance(_ block: () -> Void)を使って色を更新するコードを括ることが推奨されていますが、それより前のシステムでは、以下のようにeffectiveAppearanceを手動で差し替える等のテクニックにより対処します。

なお、NSAppearance.currentはmacOS 13以降では非推奨となっています。

extension NSAppearance {
	
	/// アピアランスを適用
	static func perform(_ closure: () -> (Void)) {
		if #available(macOS 11.0, *) {
			NSApp.effectiveAppearance.performAsCurrentDrawingAppearance(closure)
		}
		else {
			let prevAppearance = NSAppearance.current
			NSAppearance.current = NSApp.effectiveAppearance
			defer {
				NSAppearance.current = prevAppearance
			}
			closure()
		}
	}
	
	/// ダークモード判定
	static func isDarkMode() -> Bool {
		if #available(macOS 10.14, *) {
			return NSApp.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
		}
		return false
	}
	
}

使用

以下のコードを適切な場所で実行すると、現在のアピアランスに基づくカラーを反映することができます。基本的には色を反映する箇所ではこのクロージャで該当コードを括っておけば良いと思われます。

※再描画が実行される代表的なメソッド等。NSViewならdrawRect(_:)、CALayerならdisplay()など。いずれもsetNeedsDisplay()のような更新フラグを立てるメソッドを使って、システムが適切な時期に実行する。

// CALayerの背景に現在のアピアランスに基づくシステムカラーを適用
NSAppearance.perform {
	layer.backgroundColor = NSColor.systemRed.cgColor
}

// NSTextViewのテキストに現在のアピアランスに基づくシステムカラーを適用
NSAppearance.perform {
	textView.textColor = NSColor.secondaryLabelColor
}

参考

https://developer.apple.com/documentation/appkit/nsappearance/performascurrentdrawingappearance(_:)
https://stackoverflow.com/questions/52504872/updating-for-dark-mode-nscolor-ignores-appearance-changes
https://christiantietze.de/posts/2021/10/nscolor-performAsCurrentDrawingAppearance-resolve-current-appearance/

Discussion