macOS 暗黑模式
之前写了《iOS 13下暗黑(深色)模式的配置》一篇关于‘iOS的暗黑模式适配’的文章~
这次讨论一下macOS环境下的暗黑模式~(macOS 10.14)
当暗黑模式是活跃状态时,代码上可更新颜色,图像和行为—以便让应用程序可以自动适应。
在macOS和iOS中,用户可以选择采用全系统的明亮或暗黑界面外观。这种被称为“暗黑模式”(dark Mode)的黑色外观,实现了许多应用程序已经采用的界面风格。用户可以选择他们喜欢的明亮或暗黑界面外观,也可以根据环境照明条件或特定的时间表来选择切换他们的界面。
图片来自Apple选择 模式(浅色/深色/自动)
进入'系统偏好设置'的'通用'项—就可以选择相应的模式(浅色/深色/自动)了~
'系统偏好设置'的'通用'各种模式(浅色/深色/自动)及选中效果,如下:
浅色 深色 自动—根据'根据环境照明条件'或'特定的时间表'
关于 系统颜色
如下,macOS、iOS、tvOS的系统颜色:
macOS system Colors iOS system Colors tvOS system Colors
讨论下macOS的系统颜色,及其在相应模式下的展示效果
查看颜色(Nib控件中查看)
在Nib控件中,查看颜色:在'ViewController.swift'文件中:(在
viewDidAppear
方法内书写如下逻辑代码)override func viewDidAppear() { super.viewDidAppear() let title_color_Arr: [String] = ["NSColor.labelColor", "NSColor.secondaryLabelColor", "NSColor.tertiaryLabelColor", "NSColor.quaternaryLabelColor", "NSColor.systemRed", "NSColor.systemGreen", "NSColor.systemBlue", "NSColor.systemOrange", "NSColor.systemYellow", "NSColor.systemBrown", "NSColor.systemPink", "NSColor.systemPurple", "NSColor.systemTeal", "NSColor.systemIndigo", "NSColor.systemGray", "NSColor.linkColor", "NSColor.placeholderTextColor", "NSColor.windowFrameColor", "NSColor.selectedMenuItemTextColor", "NSColor.alternateSelectedControlTextColor", "NSColor.headerTextColor", "NSColor.separatorColor", "NSColor.gridColor", "NSColor.textColor", "NSColor.textBackgroundColor", "NSColor.selectedTextColor", "NSColor.selectedTextBackgroundColor", "NSColor.selectedTextColor", "NSColor.selectedTextBackgroundColor", "NSColor.unemphasizedSelectedTextBackgroundColor", "NSColor.unemphasizedSelectedTextBackgroundColor", "NSColor.windowBackgroundColor", "NSColor.underPageBackgroundColor", "NSColor.controlBackgroundColor", "NSColor.selectedContentBackgroundColor", "NSColor.unemphasizedSelectedContentBackgroundColor", "NSColor.findHighlightColor", "NSColor.controlColor", "NSColor.controlTextColor", "NSColor.selectedControlColor", "NSColor.selectedControlTextColor", "NSColor.disabledControlTextColor", "NSColor.keyboardFocusIndicatorColor", "NSColor.controlAccentColor"] let v_color_Arr: [NSColor] = [NSColor.labelColor, NSColor.secondaryLabelColor, NSColor.tertiaryLabelColor, NSColor.quaternaryLabelColor, NSColor.systemRed, NSColor.systemGreen, NSColor.systemBlue, NSColor.systemOrange, NSColor.systemYellow, NSColor.systemBrown, NSColor.systemPink, NSColor.systemPurple, NSColor.systemTeal, NSColor.systemIndigo, NSColor.systemGray, NSColor.linkColor, NSColor.placeholderTextColor, NSColor.windowFrameColor, NSColor.selectedMenuItemTextColor, NSColor.alternateSelectedControlTextColor, NSColor.headerTextColor, NSColor.separatorColor, NSColor.gridColor, NSColor.textColor, NSColor.textBackgroundColor, NSColor.selectedTextColor, NSColor.selectedTextBackgroundColor, NSColor.selectedTextColor, NSColor.selectedTextBackgroundColor, NSColor.unemphasizedSelectedTextBackgroundColor, NSColor.unemphasizedSelectedTextBackgroundColor, NSColor.windowBackgroundColor, NSColor.underPageBackgroundColor, NSColor.controlBackgroundColor, NSColor.selectedContentBackgroundColor, NSColor.unemphasizedSelectedContentBackgroundColor, NSColor.findHighlightColor, NSColor.controlColor, NSColor.controlTextColor, NSColor.selectedControlColor, NSColor.selectedControlTextColor, NSColor.disabledControlTextColor, NSColor.keyboardFocusIndicatorColor, NSColor.controlAccentColor] //NSColor.alternatingContentBackgroundColors //[NSColor]数组 //let total_W: CGFloat = self.view.window?.contentView?.frame.size.width ?? 0//获取到的窗口宽度——需要在`viewDidAppear`方法中 let total_W: CGFloat = 1500.0//总宽度 let margin: CGFloat = 25.0 let countNum: CGFloat = 5//每一行里面的个数 let item_W: CGFloat = (total_W - (countNum + 1)*margin)/countNum let item_H: CGFloat = 30 for i in 0..<v_color_Arr.count { print(i) let x = CGFloat(i%Int(countNum)) * (item_W + margin) + margin let y = CGFloat(i/Int(countNum)) * (item_H + margin) + margin let showV = NSView(frame: NSMakeRect(x, y, item_W, item_H)) self.view .addSubview(showV) showV.wantsLayer = true let showColor = v_color_Arr[i] showV.layer?.backgroundColor = showColor.cgColor showV.layer?.borderWidth = 2.0; showV.layer?.borderColor = NSColor.red.cgColor let lb_margin: CGFloat = 3.0 let lb_W: CGFloat = item_W let lb_H: CGFloat = margin - 2*lb_margin let tf = NSTextField(frame: NSMakeRect(showV.frame.minX, showV.frame.minY - lb_margin - lb_H, lb_W, lb_H)) self.view .addSubview(tf) tf.isEditable = false tf.stringValue = title_color_Arr[i]//标题——该颜色 tf.backgroundColor = NSColor.red } }
1.'浅色'模式时,运行工程——启动App
1-1.'浅色'模式时启动App
1-1.'浅色'模式时启动App1-2.'浅色'模式时启动App,再在'通用"中切为'深色'模式
1-2.'浅色'模式时启动App,再切为'深色'模式
2.'深色'模式时,运行工程——启动App
2-1.'深色'模式时启动App
2-1.'深色'模式时启动App2-2.'深色'模式时启动App,再在'通用"中切为'浅色'模式
2-2.'深色'模式时启动App,再切为'浅色'模式
结论:
a.部分颜色在'深色'模式和'浅色'模式下展示不同!(labelColor
、underPageBackgroundColor
、selectedControlTextColor
等……)
b.还有少部分颜色在App运行后,再切换'深色'/'浅色'模式时会有变化!(separatorColor
、secondaryLabelColor
、tertiaryLabelColor
、quaternaryLabelColor
等……)
[A].在UI上设置颜色:
浅色和深色界面模式使用非常不同的调色板。在浅色下效果很好的颜色在深色下可能很难被看到,反之亦然。一个自适应颜色对象为不同的界面模式返回不同的颜色值。
如下,有两种方法可以创建自适应颜色对象:
-
选择语义颜色(semantic colors),而不是固定的颜色值。在配置UI元素时,选择具有
labelColor
之类名称的颜色。这些语义颜色传达了颜色的预期用途,而不是特定的颜色值。当将它们用于预期目的时,它们将以适合当前设置的颜色值来呈现。要获取语义颜色名称的完整列表,请参见NSColor和UIColor。
为自定义UI元素使用语义颜色,以便它们与其他AppKit视图的外观匹配!参 UI Element Colors -
在资产目录(asset catalog)中定义所需自定义颜色。当你需要一个特定的颜色,创建它作为一个颜色资产(color asset)。在定义的资产中,为浅色和深色的外观指定相应的不同颜色值。还可以指定颜色的高对比度版本。
选择'Color Set'项 为各种模式配置相应颜色
在'Assets.xcassets'中添加自定义颜色:(点击+,选择'Color Set'项)注:使用Any Appearance变量指定在不支持暗黑模式的旧系统上使用的颜色值。
代码使用时,按名称来加载该颜色:
override func viewDidLoad() { super.viewDidLoad() let defineColor_V = NSView(frame: NSMakeRect(20, 10, 150, 50)) self.view .addSubview(defineColor_V) defineColor_V.wantsLayer = true //let useColor = NSColor(named: "GYHViewColor")//有效果 let useColor = NSColor(named: NSColor.Name("GYHViewColor"))//官方推荐的写法 defineColor_V.layer?.backgroundColor = useColor?.cgColor }
效果:在'浅色'模式和'深色'模式下,启动App—颜色不同、启动App后切换'浅色'模式和'深色'模式—颜色 不会发生变化
'浅色'模式时启动App '深色'模式时启动App从颜色资产(color asset)创建一个颜色对象时,不必在当前外观发生变化时重新创建该对象。每次设置绘图的填充(fill)或描边(stroke)颜色时,颜色对象就会加载与当前环境设置相匹配的颜色变量。对于语义颜色(如
labelColor
)也是如此,它会自动适应当前环境。相比之下,使用固定组件值创建的颜色对象不会自适应;你必须创建一个新的颜色对象。
Tips:使用特定方法来更新 自定义视图的颜色
类及其方法
当用户更改系统外观时,系统会自动要求每个窗口和视图重新绘制自己。在此过程中,系统会调用下表中列出的几个macOS和iOS常用方法来更新内容。
在这些方法之外进行了外观敏感更改,那么应用程序可能无法为当前环境正确绘制其内容。解决方案是:将外观敏感更改的代码都移入到这些方法中。🌰例子
自定义一个视图类GYHDefineView:
GYHDefineView在'GYHDefineView.swift'文件中,重写
draw
方法:(进行外观敏感更改的代码放在里面)import Cocoa class GYHDefineView: NSView { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. self.wantsLayer = true //let useColor = NSColor(named: "GYHViewColor")//有效果 let useColor = NSColor(named: NSColor.Name("GYHViewColor"))//官方推荐的写法 self.layer?.backgroundColor = useColor?.cgColor } }
在'ViewController.swift'文件中使用GYHDefineView实例~
override func viewDidLoad() { super.viewDidLoad() let defineColor_V = GYHDefineView(frame: NSMakeRect(20, 10, 150, 50)) self.view .addSubview(defineColor_V) }
效果:启动App后切换'浅色'模式和'深色'模式—颜色 会发生变化
[B].为图片去配置相应'浅色'/'深色'模式下的展示图(启动App后切换'浅色'模式和'深色'模式—展示图 会发生变化)
图片配置相应'浅色'/'深色'模式下的展示图:(如下,配置好'Appearance')
未配置'Appearance'时 配置好了'Appearance'的图片代码使用:
override func viewDidLoad() {
super.viewDidLoad()
let defineColor_V = NSView(frame: NSMakeRect(20, 10, 150, 50))
self.view .addSubview(defineColor_V)
defineColor_V.wantsLayer = true
//let useColor = NSColor(named: "GYHViewColor")//有效果
let useColor = NSColor(named: NSColor.Name("GYHViewColor"))//官方推荐的写法
defineColor_V.layer?.backgroundColor = useColor?.cgColor
let imgV = NSImageView(frame: NSMakeRect(20, 10, 150, 50))
self.view .addSubview(imgV)
imgV.image = NSImage(named: "Picture")
}
效果:启动App后切换'浅色'模式和'深色'模式—展示图 会发生变化
更多,参考《Providing Images for Different Appearances》
[C].代码判断当前是否为暗黑模式:
//判断——当前是否为暗黑模式
func checkIsDark() -> Bool {
let apperance = NSApp.effectiveAppearance;
if #available(macOS 10.14, *) {
if apperance .bestMatch(from: [NSAppearance.Name.darkAqua, NSAppearance.Name.aqua]) == NSAppearance.Name.darkAqua {
return true //'深色'模式
}
}
return false //'浅色'模式
}
更多NSAppearance.Name
的枚举值:
extension NSAppearance.Name {
@available(macOS 10.9, *)
public static let aqua: NSAppearance.Name
@available(macOS 10.14, *)
public static let darkAqua: NSAppearance.Name
@available(macOS, introduced: 10.9, deprecated: 10.10, message: "Light content should use the default Aqua apppearance.")
public static let lightContent: NSAppearance.Name
/* The following two Vibrant appearances should only be set on an NSVisualEffectView, or one of its container subviews.
*/
@available(macOS 10.10, *)
public static let vibrantDark: NSAppearance.Name
@available(macOS 10.10, *)
public static let vibrantLight: NSAppearance.Name
/* The following appearance names are for matching using bestMatchFromAppearancesWithNames:
Passing any of them to appearanceNamed: will return NULL
*/
@available(macOS 10.14, *)
public static let accessibilityHighContrastAqua: NSAppearance.Name
@available(macOS 10.14, *)
public static let accessibilityHighContrastDarkAqua: NSAppearance.Name
@available(macOS 10.14, *)
public static let accessibilityHighContrastVibrantLight: NSAppearance.Name
@available(macOS 10.14, *)
public static let accessibilityHighContrastVibrantDark: NSAppearance.Name
}
[D].通过代码设置当前模式为'深色'模式、'浅色'模式:
(设置NSApplication .shared.appearance
或NSApp.appearance
属性)
//设置——'深色'模式、'浅色'模式
func setNowToIsDark(isDark: Bool) {
if isDark {
NSApplication .shared.appearance = NSAppearance(named: NSAppearance.Name.darkAqua)
//NSApp.appearance = NSAppearance(named: NSAppearance.Name.darkAqua)
} else {
NSApplication .shared.appearance = NSAppearance(named: NSAppearance.Name.aqua)
//NSApp.appearance = NSAppearance(named: NSAppearance.Name.aqua)
}
}
[E].选择暂时退出 暗黑模式(Opt Out of Dark Mode)
在info.plist文件中,添加'NSRequiresAquaSystemAppearance'键并设置其Boolean型为YES:
此时运行App后,界面是以'浅色'模式展示~
但此时代码上,仍然可以设置为'深色'模式、'浅色'模式(通过设置NSApplication .shared.appearance
或NSApp.appearance
属性)
[F].监听 '浅色'模式和'深色'模式的切换:(通过NSApp来监听)
如果应用程序有不属于NSView的代码,并且不能使用上面列表列出的首选方法,它可以观察 应用程序的effecveappearance属性并手动更新currentAppearance。
var observation: NSKeyValueObservation?//观察的属性
监听的代码:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
observation = NSApp.observe(\.effectiveAppearance) { (app, _) in
//print("app.effectiveAppearance:\(app.effectiveAppearance.name)")//NSAppearanceName(_rawValue: NSAppearanceNameDarkAqua)
//print("app.effectiveAppearance:\(app.effectiveAppearance.name.rawValue)")//NSAppearanceNameDarkAqua
//监听到的当前模式——进行相应操作
if app.effectiveAppearance.name == NSAppearance.Name.aqua {
//'浅色'模式——响应的操作
} else if app.effectiveAppearance.name == NSAppearance.Name.darkAqua {
//'深色'模式——响应的操作
}
if #available(OSX 11.0, *) {//macOS 11.0以上才支持
app.effectiveAppearance.performAsCurrentDrawingAppearance {
// Invoke your non-view code that needs to be aware of the
// change in appearance.
//监听到的当前模式——进行相应操作
}
}
}
}
监听到的当前模式('浅色'模式或'深色'模式)——再进行相应操作!
[G].注意
- 根据使用目的选择视觉效果材料(Visual-Effect Materials)
视觉效果视图增加了背景视图的透明度,这比背景不透明时给UI更多的视觉深度。- 在macOS中,根据在界面中使用视图的方式,用适当的材料配置一个NSVisualEffectView。 例如,当使用一个视觉效果视图作为侧边栏界面的背景时,使用NSVisualEffectMaterialSidebar材料配置它。
- 在iOS中,配置一个具有特定的毛玻璃和模糊效果(震颤式画面)的UIVisualEffectView来创建你想要的外观。模糊效果定义背景视图的表观厚度,而毛玻璃效果调整特定类型的内容的外观,以确保它们保持可见。例如,当你的视图包含标签时,选择UIVibrancyEffectStyleLabel样式或其他标签相关的毛玻璃选项之一。
- 在外观过渡动画(Appearance Transitions)期间,避免 消耗昂贵的任务
当用户界面在'浅色'模式和'深色'模式之间切换时,系统会要求你的应用程序重新绘制所有内容。虽然系统管理绘图过程,但它依赖于在该过程中自定义代码的几个点。代码必须尽可能快并且不能执行与外观更改无关的任务。在macOS中,AppKit通常会在外观更改时创建过渡动画,但如果应用程序重新绘制自己的时间太长,它会中止掉这些动画。
参考文章
Supporting Dark Mode in Your Interface:https://developer.apple.com/documentation/uikit/appearance_customization/supporting_dark_mode_in_your_interface
Choosing a Specific Appearance for Your macOS App:https://developer.apple.com/documentation/appkit/nsappearancecustomization/choosing_a_specific_appearance_for_your_macos_app
stackoverflow | How can dark mode be detected on macOS 10.14?