本文内容来自作者的WWDC视频、官方文档学习笔记,由AI进行整理
开篇摘要
自 iOS 13 起,系统提供完整的 Dark Mode 支持。多数场景下,UIKit 会根据当前外观模式自动切换颜色、重绘与重布局,开发者无需手写 if dark 分支。但在实际工程中,仍有不少路径需要自行处理。本文梳理 系统能力、语义颜色机制、Trait Collection、常见例外场景与实践检查项,便于查阅与对外分享。
一、概述
1.1 系统已经做了什么
| 能力 | 说明 |
|---|---|
| 系统语义色 | systemBackground、label 等,随 Light / Dark 自动切换(详见第三章) |
| Materials | 四种厚度的模糊材质,适用于浮层、工具栏等(详见 3.2) |
| 内置控件 | UILabel、UIButton、UITableView 等默认适配外观模式 |
| Asset Catalog | Color / Image 可配置多 Appearance,运行时自动选用对应资源 |
结论:大部分配色与资源适配工作,系统与 Asset 已覆盖,无需编码分支。
1.2 为什么工程里还要自己实现?
系统能力覆盖的是 UIKit 标准路径;实际项目常踩这些坑:
| 场景 | 原因 |
|---|---|
CGColor / Core Animation |
layer.borderColor 等不走 UIKit 语义色解析,需手动 resolvedColor |
富文本 NSAttributedString |
未设 foregroundColor 时默认固定黑,不会随外观模式变 |
自定义绘制 draw(_:) |
需在正确的 trait 时机读取颜色 |
| 历史硬编码色值 | 老代码大量 UIColor(red:green:blue:) 或固定 HEX |
| 品牌色体系 | 需在 Asset Catalog 中自建颜色集,对齐设计 token |
| 固定主题区域 | 局部 overrideUserInterfaceStyle、弹窗 elevated 层级等 |
结论 :系统帮你切换,但 非 UIKit 路径 + 自定义设计体系 + 遗留代码 仍要开发者处理。应对思路是:用语义颜色表达用途,让系统在正确的时机解析------见第二章。
二、核心机制:语义颜色(Semantic Color)
2.1 语义颜色是什么
目标:用「场景语义」描述颜色,而不是写死 HEX;Light / Dark 下由系统根据当前环境自动选用对应色值。
| 概念 | 说明 |
|---|---|
| 语义颜色 | 如「主文字色」「页面背景色」,表达 用途 而非具体色值 |
| 系统职责 | UIKit 在外观模式变化时自动重绘、重布局,开发者多数情况无需手写切换逻辑 |
常见来源有三类:
- 系统语义色 :
UIColor.label、UIColor.systemBackground等 - Asset Catalog :
UIColor(named: "brandPrimary")等为不同 Appearance 配置色值 - 代码闭包 :
UIColor { traitCollection in ... }按 trait 返回对应颜色
2.2 解析语义颜色
swift
// 手动解析当前 trait 下的真实颜色
let semanticColor = UIColor.systemBackground
let traitCollection = view.traitCollection
let resolvedColor = semanticColor.resolvedColor(with: traitCollection)
// 代码自定义语义颜色
let customSemanticColor = UIColor { traitCollection in
if traitCollection.userInterfaceStyle == .dark {
return .black
} else {
return .white
}
}
三、系统提供的适配能力
3.1 系统语义色(UI Element Colors)
常用示例:
- 背景 :
systemBackground、secondarySystemBackground、tertiarySystemBackground - 文字 :
label、secondaryLabel、tertiaryLabel - 分组 :
systemGroupedBackground等
完整目录见 Apple 文档:UI element colors
两个 Level(base / elevated):
csharp
全屏铺满边缘(如根 VC) → base level(更深,视觉靠后)
非全屏(如 present、popover)→ elevated level(更亮,视觉靠前)
由 UITraitCollection.userInterfaceLevel 控制,系统控件会自动选对层级。
3.2 Materials(材质 / 模糊)
四种厚度,本质是 带模糊的背景材质,常用于浮层、工具栏、侧边栏等:
| 类型 | 特点 |
|---|---|
ultraThin |
最透、模糊最轻 |
thin |
较透 |
regular |
默认常用 |
thick |
最不透明、模糊最重 |
与纯色背景不同:Materials 叠加在内容之上,产生 毛玻璃 + 自适应色调,Light / Dark 下系统自动调参。
Materials 是什么?
不是单纯颜色,而是 UIBlurEffect + 系统调色 的组合视觉层;比
systemBackground更适合浮在内容上的 UI。
3.3 内置 Views / Controls
UILabel、UIButton、UITableView 等默认使用系统语义字色、背景色,开箱适配 Dark Mode。
3.4 Asset Catalog
在 .xcassets 中为 Color / Image 勾选 Appearances: Any, Dark(还可加 High Contrast 等),运行时:
swift
let color = UIColor(named: "customControlColor")
let image = UIImage(named: "icon_home")
大部分配色工作可在 Asset 完成,无需写 if dark 分支。
四、Trait Collection 机制
4.1 关键属性
存储在 UITraitCollection 中:
| 属性 | 含义 |
|---|---|
userInterfaceStyle |
.light / .dark / .unspecified |
userInterfaceIdiom |
iPhone / iPad 等 |
userInterfaceLevel |
.base / .elevated(背景深浅层级) |
| 其他 | 尺寸类、布局方向、对比度、动态字体等 |
4.2 UITraitCollection.current
- 系统在
draw、layoutSubviews等回调里 自动更新current - 在这些方法内读
UIColor.systemBackground等是安全的 - 在这些方法之外 读
current无法保证正确
4.3 外观模式切换时的调用链(简化)
scss
trait 变化
→ setNeedsLayout / setNeedsDisplay
→ updateConstraints / layoutSubviews / draw(_:)
→ traitCollectionDidChange(_:) // 最后执行
官方最佳实践 :在 layoutSubviews 系列方法中读取 traitCollection 做适配。
4.4 iOS 13 行为变化:Trait 预测
- 创建
UIView/UIViewController时,系统 预测 将要添加到的环境,先赋一个 trait - 真正 add 到父视图后 继承父视图 trait ;预测正确则
traitCollectionDidChange不触发 - Debug 时可在 Scheme → Run → Arguments 添加启动参数
-UITraitCollectionChangeLoggingEnabled YES,查看 Trait 变化日志
五、无法自动适配的场景与解法
5.1 CGColor / CALayer
layer.backgroundColor = UIColor.label.cgColor 不会随外观模式自动更新。
三种解法(推荐前两种):
swift
// 1. resolvedColor(推荐)
layer.borderColor = UIColor.label.resolvedColor(with: traitCollection).cgColor
// 2. performAsCurrent
traitCollection.performAsCurrent {
layer.borderColor = UIColor.label.cgColor
}
// 3. 临时设置 current(后台线程也安全,但写法略 hack)
UITraitCollection.current = traitCollection
layer.borderColor = UIColor.label.cgColor
5.2 富文本
UILabel.attributedText 未指定 foregroundColor → 固定黑色。
解决 :将 foregroundColor 设为语义颜色(Asset 或 UIColor { } 闭包)。
5.3 多 Appearance 图片
UIImageView按 自身traitCollection解析 Asset 图片(不是UITraitCollection.current)- 手动解析:
swift
let image = UIImage(named: "sd")
let asset = image?.imageAsset
let resolvedImage = asset?.image(with: traitCollection)
- 运行时可向
imageAsset注册不同 appearance 的图片
5.4 Web 内容
CSS:color-scheme、prefers-color-scheme
详见 WWDC:Supporting Dark Mode in Web Content
六、自定义视图:在正确时机更新
外观模式切换时,系统会先更新 UITraitCollection,再按固定顺序调用布局/绘制相关方法。
在这些方法里读取 trait、解析语义颜色是安全的 ;在方法之外做依赖外观模式的绘制(尤其是 CGColor)则无法保证正确。
各组件适用方法对照表
| Class | Appropriate methods |
|---|---|
UIView |
traitCollectionDidChange(_:)、layoutSubviews()、draw(_:)、updateConstraints()、tintColorDidChange() |
UIViewController |
traitCollectionDidChange(_:)、updateViewConstraints()、viewWillLayoutSubviews()、viewDidLayoutSubviews() |
UIPresentationController |
traitCollectionDidChange(_:)、containerViewWillLayoutSubviews()、containerViewDidLayoutSubviews() |
NSView(macOS) |
updateLayer()、draw(_:)、layout()、updateConstraints() |
七、控制与覆盖
| 能力 | 用法 |
|---|---|
| 单 View / VC 固定 mode | overrideUserInterfaceStyle = .light / .dark |
| 全 App 固定 mode | Info.plist → UIUserInterfaceStyle |
| 自定义 trait 传递 | UIPresentationController / UIViewController 重写 traitCollection |
| Status Bar | iOS 13+ default 变为随外观模式变化(Light 黑字 / Dark 白字) |
| Activity Indicator | 用 medium / large;可用 color 自定义 |
八、HIG 设计准则摘要
8.1 通用原则
- 不要做 App 内独立的「主题开关」(与系统 Light / Dark 脱节会困扰用户)
- 两种外观模式下都要好看、可读
- 测试 增加对比度 、降低透明度 等辅助功能下的表现
- 少数沉浸式场景可仅深色(如媒体播放)
8.2 颜色
- 拥抱语义颜色;自定义色放 Asset Catalog
- 对比度:正文至少 4.5:1 ;小字/自定义控件建议 7:1
- 白色背景上的图文可适当压暗,避免 Dark 下光晕感
8.3 图标与图片
- 优先 SF Symbols(多 appearance 适配)
- 必要时为 Light / Dark 各做一套图标
- 全彩图需在两种外观模式下都验收
8.4 文字
- 用系统
label系列语义色 - 优先系统
UITextField/UITextView,少自绘
8.5 平台
- Dark Mode:iOS / iPadOS / macOS ;不支持 visionOS / watchOS
- 多窗口、多任务下系统自动调整 base/elevated
九、工程实践检查清单
objectivec
□ 颜色:优先系统语义色 + Asset Catalog 自定义 token
□ 禁止:大量硬编码 HEX / 仅 Light 稿取色
□ Layer:CGColor 在 layoutSubviews / traitCollectionDidChange 里 resolved
□ 富文本:foregroundColor 用语义色
□ 图片:Asset 多 appearance;SF Symbols 优先
□ 测试:Light / Dark / 增加对比度 / 降低透明度 / popover 层级
□ 时机:在 layoutSubviews 系列方法读 trait,别在随意异步回调里读 current
□ 局部固定主题:overrideUserInterfaceStyle,而非全局 if-else
十、知识地图
objectivec
语义颜色(表达用途)
↓
语义 UIColor(闭包 / Asset / 系统色)
↓
UITraitCollection(style + level + 辅助功能)
↓
UIKit 自动:layout / draw / traitCollectionDidChange
↓
例外手动:CGColor、富文本、自定义 CA、Web
上图将全文串联为一条因果链:先建立「系统做什么、例外在哪」的整体模型,再回头查具体章节;排查问题时,也可据此判断落在哪一层(色未配对、trait 时机不对,还是 CGColor 等例外路径)。