大多数浏览器和
Developer App 均支持流媒体播放。
-
将 SwiftUI 与 AppKit 搭配使用
探索快捷指令 App 如何搭配使用 SwiftUI 和 AppKit 在 macOS 上打造一流的体验。和快捷指令团队一起,跟着我们学习如何在 AppKit 代码中托管 SwiftUI 视图,调整布局和尺寸,加入响应者链,启用导航专注,以及更多。我们还将说明如何托管 AppKit 视图,帮助您在 App 中将现有代码迁移到 SwiftUI 布局。
资源
相关视频
WWDC22
WWDC21
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 欢迎收看 “ SwiftUI 与 AppKit 组合使用方法” 我是 Ian 是快捷指令的工程师 在 macOS Monterey 中 上线了macOS版的快捷指令 App 快捷指令 App 在 Mac 上 大量地采用 SwiftUI 来实现 SwiftUI 有助于为平台定制体验 同时在 iOS 和 watchOS 上 与 App 共享常用视图 本次视频 我将用 快捷指令 App 的例子向大家展示 在 Mac 的 App 中 采用 SwiftUI 的方法 首先 我将向您展示 如何在 App 中 托管 SwiftUI 视图 然后为您讲解如何在 AppKit 和 SwiftUI 之间传输数据 还有如何在集合或表格视图的 单元格中 引入 SwiftUI 视图 当 SwiftUI 视图 嵌入到 AppKit 中时 如何处理视图的布局和大小 如何将您的 SwiftUI 视图加入到 响应链中并获得焦点 以及如何在 SwiftUI 中 引入 AppKit 视图 好 我先从在 AppKit 中 引入 SwiftUI 开始 在快捷指令 App 中 主窗口包含一个 AppKit 分割视图控制器 左边的侧边栏 是用 SwiftUI 编写的 侧边栏视图 就是一个 SwiftUI 列表 列表中显示的各行可以 导航至相应条目 视图通过被选中的绑定条目 来追踪所选中的条目 可能选中的条目 在 SidebarItem 类型中 显示为案例 在此情况下 由于已经有了一个分割视图控制器 要引入该侧边栏视图 我们要用 SwiftUI 中一个名为 NSHostingController 的类 SwiftUI 侧边栏视图 作为该主控制器的根视图被导入 由于主控制器 同任一视图控制器一样可被利用 在这里 我们将其配置为 splitViewItem 并添加到 splitViewController 现在分屏浏览引入了侧边栏 但要让其在选项变化时发挥作用 分割视图的右侧需要显示不同的页面 现在 所选条目状态 仅存在于 SwiftUI 中 我们需要将其移动到分屏浏览和 侧边栏均可共享的位置 最好的办法是创建一个可以存储在 SwiftUI 之外的模型对象 并包含 需要共享的状态 我将这个对象称为 SelectionModel 现在 侧边栏仍可在 SelectionModel 中 保持读写状态 在代码中 SelectionModel 是一个符合 ObservableObject 的类 作为可观察对象 当模型中存储的状态发生变化时 SwiftUI 就会重新加载视图 它还存储了当前选中的侧边栏条目 发布此属性 可让 SwiftUI 侧边栏视图 在所选条目更改时同步更新 每次更改侧边栏中的选项时 该模型就可在细节视图中显示新页面 介绍完在 AppKit 中 引入 SwiftUI 的方法 接下来看集合和表格单元格 将快捷指令 App 从其他平台带到 macOS 时 就有一个标志性的 SwiftUI 视图 可在集合视图单元格 或主屏幕小组件中 显示快捷方式 在 macOS 上 这些相同的视图显示在 NSCollectionView 的单元格中 在包含大量条目的集合或表格视图中 随着滚动 每个单元格视图都会被回收 显示不同的内容 为保证单元重用良好运作 您要避免在用户滚动时 从单元格中添加和删除子视图 在每个单元格中 显示 SwiftUI 视图时 要使用单个托管视图 并在 单元格内容需要改变时 用不同的根视图更新 您需用以下方法构建集合视图单元 以托管 SwiftUI 在这个例子中 我正在构建 显示快捷指令视图的单元格 每个单元格都包含一个 NSHostingView 来托管 SwiftUI 由于单元格创建于 添加任何内容之前 所以要从零开始 并在第一次准备 显示快捷指令时设置 displayShortcut 方法在配置单元格 以显示快捷方式时被数据源调用 该方法创建了一个 SwiftUI ShortcutView 那么 如果已经 有一个 hostingView 其 rootView 就要 设置为新视图 否则 如果是第一次显示快捷指令 就会创建一个 newHostingView 作为单元格的子视图添加其中 以下是一个主 SwiftUI 单元格的生命周期 首先 单元格被初始化 没有子视图 因为此时还未显示快捷方式 第一次调用 displayShortcut 时 hostingView 就被创建 shortcutView 也显示出来 这会创建一个 SwiftUI 视图层次结构 包含一个 Vstack 视图 一个图像视图 一个空格视图和两个文本视图 如果该单元格滚动到屏幕外 就可能会被系统从伫列中移除 并且需要显示不同的快捷指令 发生这种情况时 一个新的 ShortcutView 会被创建 并提供给 HostingView 由于 HostingView 已经显示出 不同的快捷指令视图 它将重用视图的整体结构 包括 VStack 和空格视图 并且只更新变化了的 图像 文本和背景视图 好 接下来 我们讨论一下布局和尺寸 主控制器和主视图的 固定尺寸 基于 SwiftUI 视图的 理想宽度和高度 SwiftUI 会自动创建和更新 Auto Layout 约束 AppKit 布局系统 会用其适当调整视图大小 视图也很灵活 这意味着它们支持 各种尺寸 可从小到大变化 SwiftUI 也为以下这些 创建约束 在层次结构中 嵌入 SwiftUI 主视图时 您要将自己的 Auto Layout 约束 应用到超级视图或其他相邻视图 使用框架修饰符 或其他 SwiftUI 布局 将更新所创建的约束 例如将宽度覆盖为固定大小 由于用户可以调整窗口大小 窗口则可以最小化和最大化 在设置 HostingViews 为一个窗口的顶层 contentView 时 SwiftUI 将基于显示内容自动更新 该窗口的最小和最大尺寸 这让窗口可按垂直或水平方向 调整大小 也可两者都做调整 取决于显示内容 放置在托管控制器中的 SwiftUI 视图 在以模态形式呈现时 也会根据内容调整大小 例如 您可以 轻松将 SwiftUI 视图 放在 AppKit 弹出框中 用 NSViewController 上的 popover presentation API 呈现托管控制器 如此处所示 您还可以将 SwiftUI 视图 呈现为工作表 用 presentAsSheet 方法即可实现 最后 对于模态窗口 您可以使用 presentAsModalWindow 方法 呈现一个阻止交互的窗口 关闭窗口即可结束阻止 窗口的大小会根据内容调整 macOS Ventura 中 NSHostingView 和 NSHostingController 上 新增了 API 可允许您自定义 自动添加的约束 默认情况下 托管控制器和视图 会为最小尺寸 固定尺寸 和最大尺寸创建约束 出于性能原因 您可能希望禁用其中一些约束 比如您希望视图始终具有灵活尺寸 或者您已 为 AppKit 中的周围视图 添加了约束 对于托管控制器 为让视图的理想尺寸 确定首选的内容尺寸 您可启用 preferredContentSize 选项 当您开始在 App 中 添加 SwiftUI 视图时 该视图像您 App 中的 其他视图一样 参与响应链和 焦点系统就显得非常重要 在快捷指令中 我们的编辑器以 SwiftUI 视图实现 但是编辑器需要处理主菜单中定义的 菜单栏命令 这也是在 AppKit 中实现 该命令包括剪切 复制 粘贴等 我们还实现了 我们自己的自定义菜单项 用于上下移动操作 在 AppKit 中 您的视图层次结构组成了一个 称为“响应者链”的视图链 焦点响应者称为第一响应者 选择菜单项时 该条目的选择器 会被发送至第一响应者 但若第一响应者 未响应该选择器 那选择器就会被 依次发送至下一个响应者 直到选择器被响应 或者到达 App SwiftUI 中 相当于第一响应者的 是焦点视图 可聚焦的 SwiftUI 视图 可以响应键盘输入 并处理发送至响应链的选择器 像文本字段 这样的视图已经可以获得焦点 但您可以使用可聚焦修饰符 让其他视图也获得焦点 SwiftUI 有处理常用命令的修饰符 例如复制 剪切和粘贴等 这些修饰符可将值传入 传出粘贴板 是用户从您的 App 传入传出数据的 简便方法 快捷指令编辑器 利用 onMoveCommand 和 onExit 命令修饰符 来处理方向键 和返回键 onCommand 修饰符 可用于处理 AppKit 和您的 App 定义的 自定义选择器中的任一常见选择器 在此 我们可处理来自 AppKit 的 selectAll 命令 以及在快捷方式 App 中 定义的 moveActionUp 和 moveActionDown 命令 在您的 App 中 测试焦点和键盘可导航性时 一定要确保打开键盘系统设置 并在全键盘导航开启和关闭的情况下 进行测试 因为许多控件仅在启用时才可聚焦 还有很多方法可以 让您的 App 与键盘完美配合 比如 FocusState 等 API 以及能让您用编程更改 需要聚焦的视图的焦点修饰符 要了解有关焦点和键盘的更多信息 您可以观看 “在 SwiftUI 中 直接和间接获取焦点”视频 最后 我们来讨论在 SwiftUI 中托管 AppKit 视图的方法 在某些情况下 “快捷指令” 会在 SwiftUI 布局内 托管 AppKit 视图 当您在 App 中 采用 SwiftUI 时 也可能需要 托管 AppKit 视图 其中一种情况是在 SwiftUI 快捷指令编辑器内部 其中有一个嵌入式 AppleScript 编辑器视图 由一个 AppKit 控件 和 macOS 上 其他一些系统 App 共享 SwiftUI 提供了两种 representable 协议 允许 AppKit 视图 和视图控制器 嵌入到 SwiftUI 视图层次结构中 与 SwiftUI 视图一样 Representable 协议 描述了 AppKit 视图 创建和更新的方式 由于 AppKit 中的 许多类都有委托 观察者 或依赖 KVO 或通知来观察 协议还包括一个可选的协调器对象 您可利用其配合您的视图 或视图控制器 这是托管对象的生命周期 及其协调器 我们从初始化托管视图开始 当视图即将首次显示时 会发生以下情况 SwiftUI 在初始化期间 做的第一件事 就是创建协调器 这可自行选择 但您可以定义自己的类型 并从 makeCoordinator 返回 如果您需要它来 进行委派或状态管理的话 协调器的单个实例 将在视图的生命周期内一直存在 其次 makeNSView 和 makeNSViewController 方法会有一个被调用 这是您向 SwiftUI 描述 如何创建视图新实例的地方 上下文包含了刚刚创建的协调器 如果有的话 这里就可将协调器分配为 视图的委托或其他类型的观察者 创建视图后 每当 SwiftUI 状态或 环境发生变化时 就会调用更新视图方法 在这里 您有义务更新 存储在 AppKit 视图中的 任何属性或状态 使其与周围的 SwiftUI 状态和环境 保持同步 更新方法可经常被调用 所以您对视图应尽可能 少做更改 在进行更改时 您应检查所做的更改 只重新加载视图受影响的部分 当 SwiftUI 完成显示托管视图时 就会被清除 托管视图和协调器都将被释放 而被释放之前 representable 协议 会给您提供选择来操作 如果需要 您可在其中清理状态 好了 现在您了解了生命周期 也熟悉了 representable 协议 我来向您展示 “快捷指令”在 App 中 托管自定义脚本编辑器视图的方法 脚本编辑器是一个名为 ScriptEditorView 的 NSView 编辑器中编写的代码可被访问 并可通过 sourceCode 属性进行修改 且视图可被禁用 以防止进行更改 脚本编辑器还有一个委托 每当有人更改源代码时都会 收到通知 托管 AppKit 视图时 首先思考一下 视图在 SwiftUI 中的位置 以及需要传入和传出的数据 在快捷指令中 该视图被放到编译按钮旁边的 一个容器视图中 编译按钮的处理程序需要访问 输入到视图中的源代码 源代码用 State 属性包装器 存储在 SwiftUI 中 representable 协议 需要阅读 并写入此状态 为了构建 representable 协议 首先要创建一个符合 NSViewRepresentable 的类型 因为它要托管一个 NSView 为每个需要从 SwiftUI 中配置的事物 添加属性 对于绑定源代码 这会读取和写入 存储在 SwiftUI 中的状态 您需要操作的第一个方法 是 makeNSView 这是您描述 创建视图新实例 以及进行一次性设置的地方 在此 委托被设置为协调器 稍后我将进一步介绍协调器 接下来 操作 updateNSView 这会在源代码更改时 或在 SwiftUI 环境 发生变化时调用 由于脚本编辑器在设置 sourceCode 属性时 做了很多工作 所以我们只比较视图中已经存在的值 并且仅在更改时设置属性 以避免不必要的工作 传递给 updateNSView 的上下文 包含 SwiftUI 环境 isEnabled 环境键 会被传递给 脚本编辑器上的 isEditable 属性 因此若 SwiftUI 视图层次结构的 其余部分被禁用 编辑也会被禁用 每当有人修改视图中的源代码时 源代码绑定就需要捕获新值 为此 我们将构建一个 符合 ScriptEditorViewDelegate 的协调器 协调器将存储 representable 值 包含需要更新的源代码绑定 在 sourceCodeDidChange 方法中 绑定被设置为来自视图的 新字符串值 最后 我们要告知 SwiftUI representable 构建和更新协调器的方法 首先 您需要使用 makeCoordinator 方法 构建一个新协调器 协调器与托管视图 具有相同的生命周期 和托管视图一样 添加到协调器的属性 随着 representable 的 变化更新 当存储在 representable 的值 发生变化时 由于 updateNSView 被调用 在这里 协调器上的 representable 属性就被更新 现在您已了解将 AppKit 添加到 SwiftUI 以及将 SwiftUI 添加到 AppKit 中的方法 您就可以将 SwiftUI 集成到 App 中了 从侧边栏或表格 和集合视图单元格开始 将会是不错的选择 可以确保您的视图尺寸合适 能正常处理常见命令和焦点 感谢收看 期待您的成果 ♪
-
-
1:29 - SidebarView and SidebarItem
struct SidebarView: View { @State private var selectedItem: SidebarItem var body: some View { List(selection: $selectedItem) { ... Section("Shortcuts") { ... } Section("Folders") { ... } } } } enum SidebarItem: Hashable { case gallery case allShortcuts ... case folder(Folder) }
-
1:53 - Hosting SwiftUI sidebar
let splitViewController = NSSplitViewController() let sidebar = NSHostingController(rootView: SidebarView(...)) let splitViewItem = NSSplitViewItem(viewController: sidebar) splitViewController.addSplitViewItem(splitViewItem)
-
3:06 - Sidebar selection model
class SelectionModel: ObservableObject { @Published var selectedItem: SidebarItem = .allShortcuts } // AppKit Window Controller cancellable = selectionModel.$selectedItem.sink { newItem in // update the NSSplitViewController detail }
-
4:37 - Collection view item hosting SwiftUI
class ShortcutItemView: NSCollectionViewItem { private var hostingView: NSHostingView<ShortcutView>? func displayShortcut(_ shortcut: Shortcut) { let shortcutView = ShortcutView(shortcut: shortcut) if let hostingView = hostingView { hostingView.rootView = shortcutView } else { let newHostingView = NSHostingView(rootView: shortcutView) view.addSubview(newHostingView) setupConstraints(for: newHostingView) self.hostingView = newHostingView } } }
-
7:55 - Popover presentation
viewController.present(NSHostingController(rootView: ...), asPopoverRelativeTo: rect, of: view, preferredEdge: .maxY, behavior: .transient)
-
8:15 - Sheet presentation
viewController.presentAsSheet(NSHostingController(rootView: ...))
-
8:22 - Modal window presentation
let hostingController = NSHostingController(rootView: ModalView()) hostingController.title = "Window Title" viewController.presentAsModalWindow(hostingController)
-
8:45 - Sizing options
hostingController.sizingOptions = [.minSize, .intrinsicContentSize, .maxSize]
-
10:47 - Copy, Cut, and Paste commands
Image(...) .focusable() .copyable { ... } .cuttable { ... } .pasteDestination(payloadType: Image.self) { ... }
-
11:02 - Respond to standard commands
struct ShortcutsEditorView: View { var body: some View { ScrollView { ... } .onMoveCommand { moveSelection(direction: $0) } .onExitCommand { cancelOperations() } .onCommand(#selector(NSResponder.selectAll(_:)) { selectAllActions() } .onCommand(#selector(moveActionUp(_:)) { moveSelectedAction(.up) } .onCommand(#selector(moveActionDown(_:)) { moveSelectedAction(.down) } } }
-
15:18 - Script editor
class ScriptEditorView: NSView { var sourceCode: String var isEditable: Bool weak var delegate: ScriptEditorViewDelegate? } protocol ScriptEditorViewDelegate: AnyObject { func sourceCodeDidChange(in view: ScriptEditorView) -> Void }
-
15:40 - Script editor container
struct ScriptEditorContainerView: View { @State var sourceCode: String = "" var body: some View { VStack { CompileButton { compile(code: sourceCode) } Divider() ScriptEditorRepresentable(sourceCode: $sourceCode) } } }
-
16:13 - Script editor representable
struct ScriptEditorRepresentable: NSViewRepresentable { @Binding var sourceCode: String func makeNSView(context: Context) -> ScriptEditorView { let scriptEditor = ScriptEditorView(frame: .zero) scriptEditor.delegate = context.coordinator return scriptEditor } func updateNSView(_ nsView: ScriptEditorView, context: Context) { if sourceCode != scriptEditor.sourceCode { scriptEditor.sourceCode = sourceCode } scriptEditor.isEditable = context.environment.isEnabled context.coordinator.representable = self } func makeCoordinator() -> Coordinator { Coordinator(representable: self) } } class Coordinator: NSObject, ScriptEditorViewDelegate { var representable: ScriptEditorRepresentable init(representable: ScriptEditorRepresentable) { ... } func sourceCodeDidChange(in view: ScriptEditorView) { representable.sourceCode = view.sourceCode } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。