iOS Dark Mode 适配笔记

本文内容来自作者的WWDC视频、官方文档学习笔记,由AI进行整理

开篇摘要

自 iOS 13 起,系统提供完整的 Dark Mode 支持。多数场景下,UIKit 会根据当前外观模式自动切换颜色、重绘与重布局,开发者无需手写 if dark 分支。但在实际工程中,仍有不少路径需要自行处理。本文梳理 系统能力、语义颜色机制、Trait Collection、常见例外场景与实践检查项,便于查阅与对外分享。


一、概述

1.1 系统已经做了什么

能力 说明
系统语义色 systemBackgroundlabel 等,随 Light / Dark 自动切换(详见第三章)
Materials 四种厚度的模糊材质,适用于浮层、工具栏等(详见 3.2)
内置控件 UILabelUIButtonUITableView 等默认适配外观模式
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.labelUIColor.systemBackground
  • Asset CatalogUIColor(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)

常用示例:

  • 背景systemBackgroundsecondarySystemBackgroundtertiarySystemBackground
  • 文字labelsecondaryLabeltertiaryLabel
  • 分组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

UILabelUIButtonUITableView 等默认使用系统语义字色、背景色,开箱适配 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

  • 系统在 drawlayoutSubviews 等回调里 自动更新 current
  • 在这些方法内读 UIColor.systemBackground 等是安全的
  • 在这些方法之外current 无法保证正确

4.3 外观模式切换时的调用链(简化)

scss 复制代码
trait 变化
  → setNeedsLayout / setNeedsDisplay
  → updateConstraints / layoutSubviews / draw(_:)
  → traitCollectionDidChange(_:)   // 最后执行

官方最佳实践 :在 layoutSubviews 系列方法中读取 traitCollection 做适配。

4.4 iOS 13 行为变化:Trait 预测

  1. 创建 UIView / UIViewController 时,系统 预测 将要添加到的环境,先赋一个 trait
  2. 真正 add 到父视图后 继承父视图 trait ;预测正确则 traitCollectionDidChange 不触发
  3. 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-schemeprefers-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 等例外路径)。


参考

相关推荐
星心源七境6 天前
七境体系全解析:从六韬兵法到AI锁颜,一套贯穿古典智慧与现代应用的成长操作系统
人工智能·设计模式·设计
三翼鸟数字化技术团队6 天前
【自研】UI还原度实时检查工具
ui kit
广州智造7 天前
如何在HyperMesh的两片相邻体单元间批量创建RBE3实现载荷传递
人工智能·设计·建模·网格·网格划分·hypermesh·前处理
_code_bear_10 天前
如何设计 Agent 场景下的 Prompt
程序员·开源·设计
湖南精循科技11 天前
Ansys 案例研究 | 刹车片应力变形仿真
设计·仿真·ansys·机械·cae·大变形
bryant_meng12 天前
【Design Patterns】23 Design Patterns: The Ultimate Developer‘s Toolkit
设计模式·编程·计算机科学·设计·工程
用户58124415415715 天前
产品经理用AI画原型,代码怎么交付?GemDesign MCP vs Claude Design Handoff 技术对比
设计
等一场雾19 天前
升级一时爽,修 Bug 火葬场:2026 年主流框架升级兼容问题血泪全记录
设计
云_杰19 天前
鸿蒙中实现果壳风格液态TabBar
harmonyos·ui kit