Unleash the UIKit trait system
Swift & UI 进阶 20m

释放 UIKit Trait System 的全部潜力

Unleash the UIKit trait system

2023年6月5日

在 Apple 官方观看视频

一句话判断

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 环境键的桥接在混编项目中非常有价值
  • traitOverrides API 支持在任意视图或视图控制器上设置覆写,不再局限于 presentation controller
  • 注册 trait 变化回调的 API 返回一个 registration 对象,持有它回调就有效,释放就自动取消
WWDC 2023