大多数浏览器和
Developer App 均支持流媒体播放。
-
释放 UIKit 特征系统的潜能
发现 UIKit 中特征系统的强大增强功能。了解如何定义自定义特征以将你的数据添加到 UITraitCollection 中,使用特征重写 API 修改传递到视图控制器和视图的数据,以及使用 API 提升灵活性和性能。此外,我们还将向你展示如何桥接 UIKit 特征以及 SwiftUI 环境键值以无缝访问 App 中 UIKit 和 SwiftUI 组件的数据。
章节
- 0:57 - Understanding traits
- 7:44 - Defining custom traits
- 14:31 - Applying overrides
- 19:48 - Handling changes
- 25:24 - SwiftUI bridging
资源
相关视频
WWDC23
-
下载
♪ ♪
Tyler:欢迎观看 “释放 UIKit 特征系统的潜力” 我是 Tyler Fox 一名 UI 框架工程师 我很高兴向你介绍一些 不可思议的 UIKit 新功能 你可以在 iOS 17 中 利用这些功能 首先 我会来回顾一下 UIKit 特征系统的基础知识 接着 我会为你介绍 其中的新功能和能力 包括定义自定义特征 以将你的数据添加到 UITraitCollection 中 更简便地在 App 的 层次结构中应用特征重写 以及更灵活地处理特征更改的情况 最后 我会解释如何桥接 UIKit 特征以及 SwiftUI 环境键值 以在你的 App 中实现 UIKit 和 SwiftUI 组件间数据的无缝传输 现在 我们来回顾一下基础知识 特征是系统自动向 App 中 所有视图控制器和视图 传递的独立数据片段 UIKit 提供了大量内置系统特征 例如 userInterfaceStyle、 horizontalSizeClass 和 preferredContentSizeCategory 在 iOS 17 中 你也可以定义自己的自定义特征 这为你提供了一个向 App 中 视图控制器和视图 传输数据的强大新方法 稍后 我会向你介绍自定义特征 使用特征集合 是你在 UIKit 中处理特征的主要方式 特征集合包括特征及其关联值 iOS 17 推出的一些新 API 可以帮助你更加 简便地处理特征集合 首先 这里有一个 带有闭包的新初始化定式 在此闭包中 你会收到一个可对其赋值的 可变特征包装器 并且 该可变特征包装器遵循 UIMutableTraits 新协议 在此闭包中 我将 userInterfaceIdiom 设置为 phone 并将 horizontalSizeClass 设置为 regular 在该闭包完成执行后 初始化定式便会返回一个 不可变的 UITraitCollection 实例 其中包含了所有 我在闭包中设置的特征值 此外 这里还有一个 modifyingTraits 新方法 可以让你通过修改闭包中的 原始特征集合值以创建新的实例 在这里 我将 horizontalSizeClass 更改为 compact 然后在 userInterfaceStyle 中 填入 dark 值 由于我没有更改 userInterfaceIdiom 所以 其值仍相当于 原始特征集合中的 phone 虽然你也可以像这样 创建自己的特征集合 但大多数时候 你还是需要 从特征环境中获取特征集合 在你的 App 中 特征环境包括窗口场景、 窗口、呈现控制器、 视图控制器以及视图 每个特征环境都有自己的特征集合 并且每个特征集合 都可能包含不同的值 特征环境在特征层次结构中 会进行连接 特征在你的 App 中 就是以这种方式进行传输的 这里是一个特征层次结构 树形结构的示例 从每个窗口场景出发 向下便会到达各自的 视图控制器和视图 并且 每个特征环境 都会从父环境中继承特征值 所以 你应该尽可能使用 来自最特定的特征环境的特征集合 接下来 我会带你深入了解特征 在视图控制器传输和视图之间的传输方式
在此示例中 这个父视图控制器 包含一个子视图控制器 其中虚线表示 视图控制器的层次结构 父控制器拥有一个视图 连接两者的实线表示二者的关系 父视图拥有一个子视图 从两者之间贯穿而过的虚线 表示视图层次 最后 子控制器的视图是 中间视图的子视图 首先 我来解释一下 在 iOS 17 以前的版本中 特征是如何 从视图控制器传递到视图的 视图控制器直接从 父视图控制器中继承特征 接着视图控制器所拥有的视图 会直接从该视图控制器中 直接继承特征 最后 没有视图控制器的视图 会直接从其父视图中继承特征 这意味着视图层次结构中的特征传输 最终会在视图控制器 所拥有的每个视图上停止 例如 父控制器视图的特征值 只能从与其 直接相连的父视图中继承 但子控制器的视图却接收不到该值 尽管其是视图层次中 这些视图的子视图 这可能会让你感到惊讶 但是在 iOS 17 中 我们对视图控制器和视图的 特征层次结构进行了统一 从而消除了该问题 现在 视图控制器就可以 从其视图的父视图中继承其特征集合 而不是直接从 其父视图控制中继承该集合 这便创建了一种 简单的线性传递方式 以让特征在 视图控制器和视图进行传输 但是请注意观察 视图控制器是如何 仍从其父视图控制器中继承特征的 只需要经由两者间的视图 即可间接实现 由于现在的视图控制器需要从 视图层次结构中继承特征 因此视图控制器的视图 必须位于层次结构中 以便让视图控制器 可以接收到更新的特征 所以 如果你在视图控制器的视图 添加到结构层次之前 访问其特征集合 那么视图控制器的特征值 将不会是最新的 你可能会发现 viewWillAppear 内部 是受影响代码最常见的地方 因为其总是在视图添加到 层次结构之前被调用 取而代之 你可使用 viewIsAppearing 新回调 viewIsAppearing 的调用 位于 viewWillAppear 之后 此时 视图已经添加到层次结构中 并且视图控制器和视图 均已获取了最新的特征集合 因此 viewIsAppearing 几乎可以直接替代 你现在使用 viewWillAppear 的所有情况 最好的一点是 这个新方法 还可以向下兼容到 iOS 13 想要进一步了解有关 这个新方法以及如何将其融合到 视图控制器生命周期中的信息 请查看“UIKit 的新功能” iOS 17 还改进了 视图特征更新的一致性和性能 视图只有在位于层次结构中时 才会更新其特征集合 并且在位于层次结构中时 每个视图只会在执行布局前 立即更新特征集合 因此 最佳实践便是 在布局过程中使用特征 对于视图而言 这就意味着需要在 LayoutSubviews 方法内部 使用 traitCollection 你需要记住的是 只要在视图上 调用 setNeedsLayout LayoutSubviews 便会再次运行 所以你的实现应当避免 由于多次调用带来的重复工作 自定义特征是 iOS 17 中 一个强大的新功能 该功能为你提供了一个全新的方法 为视图控制器和视图提供数据 当你在 App 中处理数据时 你可以考虑以下几点 来帮助确定 定义新自定义特征的时机 当你需要将数据传递给大量子类时 例如从父视图控制器 到多个子视图控制器 或是从父视图到所有子视图 特征便是一个很好的选择 同时 你还可以利用特征 将数据传递给 其他嵌套于多个层次之中的组件 即与你没有直接连接的组件 由于特征是通过层次结构继承的 所以它们可以为 你的视图和视图控制器 提供环境信息 例如提供有关 所包含视图控制器的信息 尽管特征系统十分强大 但使用其 传递数据并非没有条件限制 为了获取最佳性能 你应该在 特征是赋值的情况下进行使用 而避免在能轻松直接传递数据的 情况下进行使用 现在 你便可以开始 定义第一个自定义特征了 假设我的 App 有一个设置界面 并且我想实现一个特征 该特征标明在我的设置视图控制器中 是否包含了视图 我只需要几行代码 便可以定义一个自定义特征
首先 我会声明一个新结构 并遵循 UITraitDefinition 协议 接着 我会实现一个必需的 静态属性 defaultValue 如果没有设置任何值 则该值是特征默认值 每个特征定义都有一个关联值类型 该类型由 defaultValue 推断而来 在本例中 由于我将 defaultValue 赋值为 false 因此 经过推断 该特征值类型为 Bool 如果你此前在 SwiftUI 中 定义过自定义环境键值 那么你对此肯定十分熟悉 一旦定义了特征 你便立即可以在 UITraitCollection 和 UIMutableTraits 上 组合使用该特征和新 API 你可以将特征看作是 用于获取和设置值的键值 在新的 UITraitCollection 初始化定式中 我可以使用 UIMutableTraits 上的 下标运算符为我的特征设置一个值 接着 我可以使用 UITraitCollection 上的 下标运算符以读回该特征值 然后 添加两个简单扩展 我便可以使用标准属性语法 来访问该特征 就像访问所有系统特征那样 在不可变的 UITraitCollection 类扩展中 我声明了一个只读属性 接着 在 UIMutableTraits 协议的 扩展中 我声明了一个可读写属性 我已经添加了这些简单的扩展 接着 我便可以使用标准属性语法 从任何地方访问我的特征 你在定义自己的自定义特征时 请务必编写这些扩展 现在 我有另外一个 自定义特征的想法 想象一下 我正在 App 中 构建对自定义颜色主题的支持 我有一个 名为 MyAppTheme 的枚举类型 代表 App 支持的 4 种不同的颜色主题 首先 我会声明一个新结构 该结构遵循 UITraitDefinition 协议 接着 我会将标准主题 作为该特征的默认值 由于我想在 App 的 自定义动态颜色中使用这个新特征 所以 我会指出 该特征影响颜色外观 并且系统会在特征发生变化时 自动重绘视图 由于影响颜色外观的特征成本很高 所以你需要谨慎使用 并只将其用于不会频繁变化的特征 在这里 特征也有名称 可用于在调试过程中打印特征等 默认情况下 它会使用特征自身类型的名字 但我可以给它一个更短的名字 例如“Theme” 最后 提供标识符字符串即可 标识符可以让特征有权获得 类似编码之类的额外功能 并且 使用反向 DNS 格式 可以确保每个特征的标识符 在你的 App 中是全局唯一的
我想使用常规属性语法 以设置并获取该属性 因此 我会扩展 UITraitCollection 和 UIMutableTraits 以声明属性 就像我在之前的示例中演示的那样 以上便是我要实现 自定义主题特征所需的操作 现在 我就可以开始使用该特征了 例如 这里展示的是 如何定义根据主题 来改变外观的自定义动态颜色 我会使用动态提供者初始化定式 创建一个新的 UIColor 在闭包内 我会使用传入特征集合的主题 以决定返回颜色 现在 我便可以将该 customBackgroundColor 设置为一个视图 由于我在定义时指定了 该特征会影响颜色外观 因此 任何使用该 customBackgroundColor 的视图 都会在主题改变时自动更新 在定义特征时 你需要重点考虑的是 与特征值关联的数据类型 最佳特征都是基于值类型构建的 值类型可以是简单结构体 和枚举类型等 你应避免在 Swift 中使用 基于类构建的特征 对于特征而言 最有效的数据类型是 Bool、 Int 和 Double 或者是使用 Int 原始值的 枚举类型 枚举类型是特征中 最有用的数据类型之一 你只需要明确指定 Int 作为枚举的原始数据类型 便可以获得最大效率 此外 任何你用作特征值的 自定义结构体数据类型都应具有 一个 Equatable 协议的高效实现 由于系统会频繁比较特征值 来确定特征发生变化的时间 所以 你的相等函数 速度应该越快越好
对于那些使用 Objective-C 的 App 而言 新的特征系统功能 在其中也同样适用 在 Swift 和 Objective-C 中 自定义特征 API 并不相同 但是 你可以在 Swift 和 Objective-C 中 分别定义一个自定义特征 并都指向相同的基础数据 想要了解更多细节信息 和特殊注意事项 请参考文档 在定义了一个自定义特征后 接下来 你便需要在 App 的 特征层次结构中对特征进行数据填充
特征重写是你用于 修改特征层次结构中数据的机制 在 iOS 17 中 应用特征重写 比以往更加简单 每个特征环境类都具有一个 新的 traitOverrides 属性 包括窗口场景、窗口、视图、 视图控制器和呈现控制器 回到特征层次结构的示意图 特征重写可以改变 这个树形结构中任意位置的特征值 当你对层次结构中的其中一个 特征环境应用特征重写时 它便会修改该对象及其所有后代 特征集合中的特征值 我们以特征层次结构中 一个父特征和一个子特征的环境为例 看看特征重写是如何 对两者产生影响的 应用于父特征的特征重写 会影响父特征自身的特征集合 接着 来自父特征集合的值 会由子特征继承 最后 子特征的特征重写 会应用于其继承的值 从而生成自己的特征集合 你可以将特征重写看作可选输入 并将特征集合看作输出 而所有未经过重写的特征 都会从父类继承 接下来 我来介绍一个使用特征重写 来改变 App 指定部分颜色主题的示例 右侧是 App 特征层次结构的示意图 起初 我没有应用任何重写 来填充主题特征值 所以这些特征集合均使用默认值 即标准主题 接下来 我首先在窗口场景中 层次结构的根部 应用特征重写 traitOverrides 属性 利用 UIMutableTraits 协议 可让你轻松设置特征值 因此 你可以使用我此前介绍过的 UIMutableTraits 扩展 并利用标准属性语法 来设置自定义特征重写的值 通过将窗口场景上 traitScene 的主题设置为 pastel 该窗口场景中的所有窗口、 视图控制器和视图 现在便会在其特征集合中 继承 pastel 值 所以 通过在层次结构的 根位置设置主题 我便可以改变传递到层次结构 各处的基础值 例如 我可以读取该窗口场景内 任何视图控制器特征集合的主题 并返回 pastel 接着 我会在层次结构更深层的视图中 使用 traitOverrides 属性 来修改视图主题及其下方所有内容 在这里 我将视图中的 traitOverride 设置为 monochrome 主题 所以 monochrome 值便是 其子视图将要继承的值 从而层次结构中更高层次的 pastel 值便会得到重写 但是 你可能无法 在 traitCollection 中立即看到 traitOverrides 的变化 例如 由于视图在布局之前 更新其特征集合 所以视图中的特征重写修改 在其运行 LayoutSubviews 之前 都不会体现在其特征集合中 此外 借助 traitOverrides 属性 你还可以检查重写是否得到应用 并完整删除重写 接下来是一个切换重写的示例 该示例使用 contains 方法 检查重写是否存在 并使用 remove 方法 完全删除重写 每次调用 remove 方法 都可以删除现有重写 或在尚未设置重写时 应用新主题重写 TraitOverrides 是 设置值的输入机制 如果要读取特征值 使用 traitCollection 属性即可 在重写尚未设置时 从 traitOverrides 中进行读取 便会引发异常
你在使用 traitOverrides 时 需要牢记以下几点性能考虑 首先 每个特征重写 都会产生一定的成本 所以你应当只在需要的地方 设置 traitOverrides 还要避免设置 用不到的 traitOverrides 并且 每当你修改特征重写时 系统都需要更新层次结构中 所有后代的特征集合 因此 你应尽量减少修改 traitOverrides 的次数 最后 应用于层次结构根部类 例如窗口场景或窗口的 traitOverrides 会影响其下方所有内容 这点非常有用 并且有大量用例 将特征重写 应用到窗口场景或窗口上 但是 如果特征只能影响 层次结构中几个更深层的视图 那么请将特征重写应用到 这些视图最接近的 共同源头处即刻 例如共同的父视图或视图控制器 这样 如果层次结构中 只有一小部分使用数据 你就无需承担在整个层次结构中 传递特征的成本了 现在 你了解了如何定义特征 以及如何在层次结构中 对其填充数据 接下来 你便需要在特征值 发生改变时进行处理
traitCollectionDidChange 在 iOS 17 中已被弃用 在你实现 traitCollectionDidChange 时 系统并不知道你实际关心哪些特征 所以 它便会在特征值发生改变时 调用该方法 但是 大部分类只会使用少数特征 并且不关注其他特征发生的变化 这也就是在你添加 越来越多的自定义特征时 traitCollectionDidChange 却无法进行扩展的原因 作为替代 新的特征注册 API 更加灵活 并还能改善性能 通过对特定特征注册更改 系统可以清楚了解你所依赖的特征 这个新的 API 让你可以 使用 target-action 模式 或闭包来接收回调 并且由于你不再需要 在子类中重写方法 从而 你便可以从任何位置 轻松观察到特征更改 我首先来解释一下如何更新 traitCollectionDidChange 的 现有实现 这里是我现有的实现 请注意观察 我在 调用 updateViews 之前 是如何确认 horizontalSizeClass 的特征是否发生更改的 因为该方法只依赖于这一个特征 如果你因为要将 App 部署到 较旧的 iOS 版本中 而需要继续使用 traitCollectionDidChange 你需要确保实现会检查 你所关注的特定特征是否发生更改 现在 我使用 iOS 17 中 新的特征注册方法来替换该实现 首先 从基于闭包的方法开始 我会调用 registerForTraitChanges 并传递需要注册的特征数组 所有的系统特征 都有新的 UITrait 符号 例如 horizontalSizeClass 所具备的这个 接着 我会传递特定特征 发生更改时所调用的闭包 由于其他特征发生更改时 并没有调用该闭包 因此无需比较这里的新旧特征值 然后 将特征发生更改的对象 作为第一个参数传递给闭包 利用该参数 你无需捕获 对该对象的弱引用 当你在 self 中注册特征变化时 在这里写“self: Self”即可 同时 你还可以观察到 不同特征环境中的特征更改 在这里 我会为两个特征注册更改 horizontalSizeClass 以及我之前定义的自定义特征 ContainedInSettingsTrait 当其中一个特征在另一个视图上 发生更改时 闭包就会开始执行 我会将注册的视图类型 写为该闭包的第一个参数
这里是一个使用基于 target-action 新方法的示例 调用 registerForTraitChanges 并传递所要注册的特征数组 以及发生更改时 所要调用的 target-action 方法 target 参数是可选的 如果省略该参数 target 便和 调用 registerForTraitChanges 的 命令是同一对象 在本例中 该对象就是 self 与使用闭包方法一样 你也可以注册 其他特征环境中的更改 在这里 我在另一视图上 注册了特征更改 但设置了调用 self 的 handleTraitChange 方法 在使用 target-action 方式 注册特征更改时 你的 action 方法可以有 0 个、1 个或 2 个参数 第一个参数通常是 特征发生更改的对象 使用该参数可以获取 新的 traitCollection 第二个参数通常是更改发生前 该对象的 traitCollection 除了为单个特征进行注册 你还可以使用 新的系统特征语义集合来进行注册 例如 systemTraitsAffectingColorAppearance 可以返回可能影响 系统动态颜色解析方式的 所有系统特征 systemTraitsAffectingImageLookup 可以返回你在使用 UIImage(named:) 加载图像时 考虑到的系统特征子集 将其中一个集合直接传递给 registerForTraitChanges 以执行自定义失效操作
在你使用新方法注册特征更改时 注册便会自动清空 如果你有一个高级用例 你便可以使用 每个注册方法返回的令牌 来手动取消注册 但这些情况非常少见 所以 一般情况下 当你调用 registerForTraitChanges 时 你应该直接忽略其返回值 当你使用新的特征注册 API 时 请牢记两个最佳实例 第一个 仅对你实际 依赖的特征进行注册 这样 在无关特征值发生更改时 你无需执行工作 第二个 尝试在特征更改后进行 无需立即更新的失效操作 例如 如果你在视图子类的 layoutSubviews 方法中使用特征 你可以调用 setNeedsLayout 以使特征更改失效 这样 视图就会接收 layoutSubviews 但不会立刻执行更新 现在 你便可以在 UIKit 中 使用特征系统来传递自己的数据 这是你在 App 中实现 UIKit 和 SwiftUI 组件间 无缝传输数据的全新方法 在 UIKit 中自定义特征和 在 SwiftUI 中自定义环境键值十分类似 你可以将两者进行桥接 以便可以 从 UIKit 和 SwiftUI 中访问相同的数据 无论你是在 UIKit 中 嵌入 SwiftUI 组件 还是在 SwiftUI 中嵌入 UIKit 组件 桥接后的数据都可以 在两者间进行无缝传输 你可以使用 UIKit 代码中的特征 API 和 SwiftUI 代码中的环境 API 来读写相同的底层数据 因此 采用我在 App UIKit 代码中定义的 新颜色主题特征 并将其桥接到 SwiftUI 中 相应的环境键值中 这样做十分简单
假设我在 UIKit 中有一个自定义特征 在 SwiftUI 中有一个自定义环境键值 两者均表示相同的数据 而我只需要在 UITraitBridgedEnvironmentKey 协议中添加一个遵循 便可以实现两者的桥接 为此 我需要实现一个方法 来从 UIKit 中读取特征 并将值返回到 SwiftUI 中 并实现另一个方法 以将 SwiftUI 环境键值写入 UIKit 特征中 这样 UIKit 特征和 SwiftUI 环境键值都可以访问统一的存储 这样 我便可以从 使用任一框架编写的组件中 读写相同的数据 这个示例展示了我如何使用 桥接后的特征和环境键值 在 App 的根部 我在 UIKit 窗口场景 对主题特征应用了一个特征重写 从而 该重写便会将 monochrome 主题值传递到 窗口场景所包含的所有内容中 接着 在窗口场景中 该窗口内部更深层处 我设置了一个 UIKit 集合视图 该集合视图包含 使用 UIHostingConfiguration 配置的单元格 用来在每个单元格中 显示一个 SwiftUI 视图 在 SwiftUI CellView 内部 我设置了一个名为“theme”的属性 该属性会使用环境属性包装器 以从 SwiftUI 环境中读取值 环境中的值对应于 UIKit 中 桥接特征的相同值 最后 我会使用主题属性 来控制该 SwiftUI 视图内部的 文本颜色 由于 SwiftUI 会自动追踪 数据依赖关系 所以 如果 UIKit 窗口场景中的 主题特征重写更改为 不同的值 那么 SwiftUI 单元格视图 便会自动更新 来显示新主题 桥接还可以在另一方面发挥作用 这里是一个显示 App 设置的 SwiftUI 视图 我会使用环境修饰符 来设置标准主题 该主题便会应用到 设置控制器中所有的内容上 这在概念上相当于 在 UIKit 中应用特征重写 然后 在包含于 UIViewControllerRepresentable 中 且基于 UIKit 的 设置视图控制器中 我会从桥接后的特征中读取主题值 并用它更新该视图控制器中 所显示的标题 以上便是使用桥接后的 UIKit 特征 和 SwiftUI 环境键值 轻松无缝访问数据的方法 现在你已经了解了 这些强大的新功能 接下来 你就可以在 App 中 找一个可以应用特征系统的位置 以通过定义自己的 自定义特征来自动数据传递 接着 使用新的 traitOverrides 属性 来轻松修改特征层次结构中的数据 然后 使用更加灵活的特征注册 API 以在你使用的特定特征中 创建精确的依赖关系 最后 桥接你的自定义 UIKit 特征 和自定义 SwiftUI 环境键值 你的数据就可以在 App 中的 UIKit 和 SwiftUI 组件间实现无缝传输 现在便是你利用特征功能的时候了 感谢你的观看 ♪ ♪
-
-
1:51 - Working with trait collections
// Build a new trait collection instance from scratch let myTraits = UITraitCollection { mutableTraits in mutableTraits.userInterfaceIdiom = .phone mutableTraits.horizontalSizeClass = .regular } // Get a new instance by modifying traits of an existing one let otherTraits = myTraits.modifyingTraits { mutableTraits in mutableTraits.horizontalSizeClass = .compact mutableTraits.userInterfaceStyle = .dark }
-
9:06 - Implementing a simple custom trait
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } let traitCollection = UITraitCollection { mutableTraits in mutableTraits[ContainedInSettingsTrait.self] = true } let value = traitCollection[ContainedInSettingsTrait.self] // true
-
10:23 - Implementing a simple custom trait with a property
struct ContainedInSettingsTrait: UITraitDefinition { static let defaultValue = false } extension UITraitCollection { var isContainedInSettings: Bool { self[ContainedInSettingsTrait.self] } } extension UIMutableTraits { var isContainedInSettings: Bool { get { self[ContainedInSettingsTrait.self] } set { self[ContainedInSettingsTrait.self] = newValue } } } let traitCollection = UITraitCollection { mutableTraits in mutableTraits.isContainedInSettings = true } let value = traitCollection.isContainedInSettings // true
-
11:00 - Implementing a custom theme trait
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true static let name = "Theme" static let identifier = "com.myapp.theme" } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } }
-
12:33 - Using a custom theme trait
let customBackgroundColor = UIColor { traitCollection in switch traitCollection.myAppTheme { case .standard: return UIColor(named: "StandardBackground")! case .pastel: return UIColor(named: "PastelBackground")! case .bold: return UIColor(named: "BoldBackground")! case .monochrome: return UIColor(named: "MonochromeBackground")! } } let view = UIView() view.backgroundColor = customBackgroundColor
-
18:05 - Managing trait overrides
func toggleThemeOverride(_ overrideTheme: MyAppTheme) { if view.traitOverrides.contains(MyAppThemeTrait.self) { // There's an existing theme override; remove it view.traitOverrides.remove(MyAppThemeTrait.self) } else { // There's no existing theme override; apply one view.traitOverrides.myAppTheme = overrideTheme } }
-
21:00 - Trait change handling on older iOS versions
// Efficient implementation that only updates when necessary override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { updateViews(sizeClass: traitCollection.horizontalSizeClass) } } func updateViews(sizeClass: UIUserInterfaceSizeClass) { // Update views for the new size class... }
-
21:28 - Registering for trait changes using a closure
// Register for horizontal size class changes on self registerForTraitChanges( [UITraitHorizontalSizeClass.self] ) { (self: Self, previousTraitCollection: UITraitCollection) in self.updateViews(sizeClass: self.traitCollection.horizontalSizeClass) } // Register for changes to multiple traits on another view let anotherView: MyView anotherView.registerForTraitChanges( [UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self] ) { (view: MyView, previousTraitCollection: UITraitCollection) in // Handle the trait change for this view... }
-
22:48 - Registering for trait changes using a target-action
// Register for horizontal size class changes on self registerForTraitChanges( [UITraitHorizontalSizeClass.self], action: #selector(UIView.setNeedsLayout) ) // Register for changes to multiple traits on another view let anotherView: MyView anotherView.registerForTraitChanges( [UITraitHorizontalSizeClass.self, ContainedInSettingsTrait.self], target: self, action: #selector(handleTraitChange(view:previousTraitCollection:)) ) @objc func handleTraitChange(view: MyView, previousTraitCollection: UITraitCollection) { // Handle the trait change for this view... }
-
24:20 - Registering for changes to system traits affecting color appearance
registerForTraitChanges( UITraitCollection.systemTraitsAffectingColorAppearance, action: #selector(handleColorAppearanceChange) ) @objc func handleColorAppearanceChange() { // Handle the color appearance trait changes... }
-
24:37 - Manually unregistering for trait changes
// Store the returned registration token let registration = registerForTraitChanges([UITraitHorizontalSizeClass.self], action: #selector(handleTraitChange)) // Later, use the stored registration token to manually unregister unregisterForTraitChanges(registration) @objc func handleTraitChange() { // Handle the trait change... }
-
26:19 - Implementing a bridged UIKit trait and SwiftUI environment key
enum MyAppTheme: Int { case standard, pastel, bold, monochrome } // Custom UIKit trait struct MyAppThemeTrait: UITraitDefinition { static let defaultValue = MyAppTheme.standard static let affectsColorAppearance = true } extension UITraitCollection { var myAppTheme: MyAppTheme { self[MyAppThemeTrait.self] } } extension UIMutableTraits { var myAppTheme: MyAppTheme { get { self[MyAppThemeTrait.self] } set { self[MyAppThemeTrait.self] = newValue } } } // Custom SwiftUI environment key struct MyAppThemeKey: EnvironmentKey { static let defaultValue = MyAppTheme.standard } extension EnvironmentValues { var myAppTheme: MyAppTheme { get { self[MyAppThemeKey.self] } set { self[MyAppThemeKey.self] = newValue } } } // Bridge SwiftUI environment key with UIKit trait extension MyAppThemeKey: UITraitBridgedEnvironmentKey { static func read(from traitCollection: UITraitCollection) -> MyAppTheme { traitCollection.myAppTheme } static func write(to mutableTraits: inout UIMutableTraits, value: MyAppTheme) { mutableTraits.myAppTheme = value } }
-
27:01 - Setting a UIKit trait and reading the bridged environment value from SwiftUI
// UIKit trait override applied to the window scene let windowScene: UIWindowScene windowScene.traitOverrides.myAppTheme = .monochrome // Cell in a UICollectionView configured to display a SwiftUI view let cell: UICollectionViewCell cell.contentConfiguration = UIHostingConfiguration { CellView() } // SwiftUI view displayed in the cell, which reads the bridged value from the environment struct CellView: View { @Environment(\.myAppTheme) var theme: MyAppTheme var body: some View { Text("Settings") .foregroundStyle(theme == .monochrome ? .gray : .blue) } }
-
28:16 - Setting a SwiftUI environment value and reading the bridged trait from UIKit
// SwiftUI environment value applied to a UIViewControllerRepresentable struct SettingsView: View { var body: some View { SettingsControllerRepresentable() .environment(\.myAppTheme, .standard) } } final class SettingsControllerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> SettingsViewController { SettingsViewController() } func updateUIViewController(_ uiViewController: SettingsViewController, context: Context) { // Update the view controller... } } // UIKit view controller contained in the SettingsControllerRepresentable class SettingsViewController: UIViewController { override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() title = settingsTitle(for: traitCollection.myAppTheme) } func settingsTitle(for theme: MyAppTheme) -> String { switch theme { case .standard: return "Standard" case .pastel: return "Pastel" case .bold: return "Bold" case .monochrome: return "Monochrome" } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。