需求分析:先想清楚哪些页面需要单独适配,哪些跟系统色走就行;哪些组件有硬编码色值需要替换。这一步直接决定后面代码改动范围和测试工作量。
关键结论:暗黑模式不是简单换几个色值,而是一套完整的配色体系和层级关系都要重新梳理。我见过不少团队以为改几个
#333就完事了,结果隔天测试跑来看,整个界面灰蒙蒙一片,对比度严重不足。设计对接:跟设计同学明确语义色规则(背景、前景、填充、分隔线各自对应哪几个层级),这一步别省,否则后面每张图、每个控件都来问你要色值,你会疯。
控件自检:逐个页面过一遍UI控件清单,拿不准的先标记,后面逐步验证。
主背景(Background):页面最底层的基础底色
次级背景(Secondary Background):卡片、分组列表的底色
三级背景(Tertiary Background):弹窗、浮层底部的底色
主文本(Primary Label):一级信息文本、标题、正文核心
次级文本(Secondary Label):辅助说明、次要信息
三级文本(Tertiary Label):占位符、时间戳、弱化信息
分隔线(Separator):列表分隔线、边框
主控件填充(Primary Fill):按钮、开关等主要控件的填充色
次级控件填充(Secondary Fill):次要控件的填充色
警示色(System Red/Orange/Yellow等):错误、警告、成功状态
// 错误示范:硬编码色值 self.view.backgroundColor = [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0]; // 正确姿势:使用系统语义色 self.view.backgroundColor = [UIColor systemBackgroundColor];打开 Assets.xcassets
在左侧栏底部点“+”号,新建 Color Set
右侧属性面板中,在 Appearance 一栏选 Any Appearance 和 Dark
分别设置 Any(浅色)和 Dark(深色)模式下的色值
代码里直接通过
[UIColor colorNamed:@"AppBackgroundColor"]调用
// 代码调用Assets颜色 self.view.backgroundColor = [UIColor colorNamed:@"AppBackgroundColor"]; // Swift写法 view.backgroundColor = UIColor(named: "AppBackgroundColor")// 动态Provider写法 + (UIColor *)appThemeColor { return [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) { if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { return [UIColor colorWithRed:0.11 green:0.11 blue:0.13 alpha:1.0]; } return [UIColor colorWithRed:0.96 green:0.96 blue:0.96 alpha:1.0]; }]; }// 关闭暗黑模式的旧做法(已被Apple官方建议逐步弃用) - (UIUserInterfaceStyle)overrideUserInterfaceStyle { return UIUserInterfaceStyleLight; }// 视图控制器级别强制 self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; // 整个窗口级别强制 self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;// 用带Luminosity的动态色替代 UIColor *navColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) { if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { return [UIColor colorWithWhite:0.12 alpha:1.0]; } return [UIColor whiteColor]; }];struct ContentView: View { @Environment(\.colorScheme) var colorScheme var body: some View { VStack { Text("标题") .foregroundColor(.primary) // 自定义颜色 RoundedRectangle(cornerRadius: 8) .fill(Color("AppBackgroundColor")) } } }| 场景 | 现象 | 排查思路 |
|---|---|---|
| 导航栏颜色发灰 | 使用了系统默认模糊效果 | 检查prefersLargeTitles与 barTintColor 是否搭配合理 |
| 图片背景变黑底 | 图片本身透明像素在深色下露出背景 | 检查图片来源、alpha通道 |
| 文本看不清 | 使用了固定前景色 | 检查 UILabel.textColor 是否硬编码 |
| 弹窗白闪 | 控制器加载时背景色未设置 | 提前设置 view.backgroundColor 或 tintColor |
| 第三方SDK页面不适配 | SDK 内部硬编码 | 等待SDK升级,或用overrideUserInterfaceStyle局部处理 |
| WebView 内容反白失败 | WebView 未设置contentMode = .always | 设置webView.isOpaque = false,配合 CSS 校验 |
| 状态栏文字不可见 | 状态栏样式与背景色冲突 | 设置preferredStatusBarStyle动态切换 |
| 阴影在深色下消失 | 阴影使用纯黑色且不透明度低 | 改用带透明度的动态色或加描边 |
以上是问题排查速查表,在实际适配过程中,你会遇到更多组合形态的怪问题。下面聊几个我印象深刻的坑。
图片切割导致的横线:某次适配后,列表里几张图片在浅色模式正常,深色模式下边缘出现一条亮线。原因是图片资源只有
Any一份,内部没有拉伸保护,在深色下边缘像素被抗锯齿放大。解决方式是让设计重新导出带安全边距的图片,或者我给图片包一层ImageView,用代码裁掉边缘透明像素。单元格复用导致的颜色串台:表格
dequeueReusableCell里如果背景色放在cellForRow里设置,切换主题后滑动列表会出现颜色闪烁,因为复用池里的 Cell 还在用旧 trait。解决方式是在layoutSubviews或prepareForReuse里重新更新颜色,别在数据回调里设置。系统控件在暗黑下的双重绘制:状态栏、导航栏这类系统控件如果既用了
barTintColor又用了动态色,深色模式下会出现双重渲染,颜色比预期更深。我后来统一走UINavigationBarAppearance设置backgroundEffect和backgroundColor,避免混用。暗黑模式下的弹窗层级:
UIAlertController在默认配置下会自动适配,但如果你的 App 整体是深色而系统是浅色,弹窗会维持系统风格,跟 App 不搭。这时只能自己定制一个弹窗视图,别指望系统弹窗完全贴合你的设计。深色模式下
UIBlurEffect的穿透问题:如果模糊层后面是个纯白视图,在深色模式下灰蒙蒙,看起来像所有内容都被蒙了一层雾。解决方式是模糊层背后加一层和背景同色系的半透明视图,或者直接用UIVibrancyEffect配合。深色模式下截图颜色不一致:用户反馈截图分享后颜色偏灰。原因是我用了系统动态色,但分享时做了位图渲染,动态色解析成了浅色值。解决方式是在分享前手动转成目标色值,或者强制以当前 trait 进行
drawHierarchy。从浅色切深色时闪白:进入设置页切换主题后,返回主页有一段白底闪烁。原因是 rootViewController 的背景色更新时机晚于视图加载。我在切换主题后主动调用
setNeedsLayout和layoutIfNeeded解决了。深色模式下图层混合模式异常:某张半透明蒙层在深色下出现明显的锯齿和噪点,因为蒙层颜色和底下图片颜色相近但混合模式不对。换成
sourceOver或调整蒙层alpha值就好。
其实这些坑说起来都小,但每一个都费了不少时间定位。下面说说我怎么验证和收尾的。
另外我还发现,直接调试真机上的暗黑模式比模拟器靠谱,模拟器某些渲染上的坑并不完全等同于真机,尤其是导航栏和模糊层,真机和模拟器的表现有明显差异。
如果你手头有测试设备,也别只测最新系统。Xcode 支持在模拟器里切换系统版本,我一般会拿一个低版本系统和一个高版本系统分别跑一遍暗黑模式,看有没有兼容性问题。虽然大部分代码是同一套,但系统版本不同,某些效果实现细节还是有差异。
最后总结几条我自己跑过无数坑后的经验和心得,希望能帮读者少走弯路。 - 首先,适配前把设计规范定清楚,别边写代码边等设计出图。 2. 优先使用系统语义色和 Assets 里的动态色,少在代码里硬编码#FFFFFF、#000000。 3. 对所有UIColor的使用做一次审查,包括第三方开源库里的色值。 4. 善用模拟器的暗黑模式一键切换功能,但最终以真机结果为准。 5. 遇到奇怪的显示效果,先排除颜色空间、混合模式、图片 alpha 通道等底层因素。 6. 多备份几套测试截图,方便回归。
这套流程我从最初的几个页面,现在已经覆盖了整个 App,前后大概花了两周多时间,改动量其实不算大,但收益很高——每次系统更新、用户反馈体验提升,都是实打实的。
适配暗黑模式的本质,是把 App 从“固定皮肤”变成“能响应环境切换的界面体系”,一旦这个架构搭起来,以后新增页面或者调整设计风格,成本都会低很多。
对读者来说,如果你们也在做 iOS 暗黑模式适配,我建议先从最小的页面开始练手,跑通全流程,再逐步推广。代码层面不要过分依赖某个“一刀切”方案,还是那句:系统语义色 + Assets 动态色 + 代码里有限的动态 Provider,这样搭配下来,既能保证兼容性,又能照顾到特殊场景。
也许有人觉得暗黑模式不是刚需,但用户端的感知是很直观的。我在搞这个适配的过程中,收到过不少用户主动发消息说“夜间模式太好用了”,这比很多隐藏功能都更有存在感。
如果后面有时间,我还会再把图片加载这一块的暗黑适配再做一轮优化,把网络图片的动态变色方案也落实下来。暂时先写到这里,希望这篇实战笔记能帮到正在做同类适配的开发者。