释放 UIKit Trait System 的全部潜力
Unleash the UIKit trait system
2023年6月5日
一句话判断
iOS 17 的 Trait System 迎来大升级:自定义 Trait、新式覆写 API、SwiftUI 桥接,让数据在视图层级中的传递有了全新的范式。
这场 Session 讲了什么
Tyler Fox 详细讲解了 UIKit Trait System 在 iOS 17 中的全面升级。内容分三个层面:
基础回顾与架构改进。Trait 是系统自动向 App 中每个视图控制器和视图传播的独立数据片段,比如界面风格、尺寸类别、字体大小等。iOS 17 统一了视图控制器和视图的 trait 层级——之前视图控制器的 trait 直接继承自父控制器,中间的视图层会”截断”传递链,现在改为线性传递,消除了这个反直觉的行为。
自定义 Trait。这是最重磅的新特性。你可以通过 UITraitDefinition 协议定义自己的 trait,比如”当前视图是否在设置页面中”。定义好后,这个自定义数据就会像系统 trait 一样自动在层级中传播。这打开了全新的数据传递模式——不需要代理、不需要闭包、不需要单例,数据就能传到嵌套很深的组件。
Trait 覆写与回调的新 API。新的闭包式 API 让你不用子类化就能覆写 trait 值和处理 trait 变化。配合 SwiftUI 环境键的桥接,UIKit 自定义 trait 和 SwiftUI 自定义环境值可以双向传递。
值得深挖的点
统一 trait 层级的行为变化。之前视图拥有的 trait 和所属视图控制器的 trait 可能不一致,因为传递链在视图控制器边界处被截断。iOS 17 统一后,视图控制器从其视图的父视图继承 trait,形成了真正的线性传递。但这意味着在 viewWillAppear 中 trait 可能还不是最新的——因为此时视图还没加入层级。新的 viewIsAppearing 才是读取 trait 的正确时机。
什么时候该用自定义 Trait。Trait 适合”一对多”的数据传播场景:父视图控制器向多个子视图控制器传递、父视图向所有子视图传递、或者向嵌套很深的组件传递上下文信息。不适合的场景:你可以直接通过属性或构造器传递数据时,就不要用 trait——trait 系统有传播开销。
与 SwiftUI 的桥接。自定义 UIKit trait 可以映射为 SwiftUI 环境键,反之亦然。这意味着在 UIKit 和 SwiftUI 混编的项目中,你可以在 UIKit 中设置一个 trait,然后在嵌入的 SwiftUI 视图中通过 @Environment 读取到。
代码片段
// 定义自定义 Trait:标记视图是否在设置页面中
struct IsInSettingsTrait: UITraitDefinition {
// 默认值决定 trait 的类型,这里用 Bool
static var defaultValue: Bool { false }
}
// 读取自定义 Trait
let isInSettings = traitCollection[IsInSettingsTrait.self]
// 在视图上设置 trait 覆写
view.traitOverrides.set(IsInSettingsTrait.self, to: true)
// 使用新的闭包式 API 处理 trait 变化(无需子类化)
let registration = view.registerForTraitChanges(
[UITraitUserInterfaceStyle.self]
) { (view, previousTraitCollection) in
// 界面风格发生变化时更新视图
let isDark = view.traitCollection.userInterfaceStyle == .dark
view.backgroundColor = isDark ? .black : .white
}
// 新的 UITraitCollection 闭包初始化器
let traits = UITraitCollection { mutableTraits in
mutableTraits.userInterfaceIdiom = .phone
mutableTraits.horizontalSizeClass = .regular
}
最佳实践
- 在
layoutSubviews中使用 trait,这是最可靠的时机。trait 更新发生在 layout 之前。 - 自定义 trait 的默认值要仔细选择——它是所有未显式设置的地方会看到的值。
- 不要在 trait 里传递频繁变化的数据(比如动画进度),trait 系统的设计目标是相对稳定的上下文信息。
- 利用 Xcode 的 trait 覆写功能测试不同配置,而不是频繁修改模拟器设置。
viewIsAppearing向后兼容到 iOS 13,可以放心替换大部分viewWillAppear的用法。
还有什么值得关注
- 这个 Session 是 “What’s new in UIKit” 的深度扩展,两个一起看效果更好
- 自定义 trait 和 SwiftUI 环境键的桥接在混编项目中非常有价值
traitOverridesAPI 支持在任意视图或视图控制器上设置覆写,不再局限于 presentation controller- 注册 trait 变化回调的 API 返回一个 registration 对象,持有它回调就有效,释放就自动取消