大多数浏览器和
Developer App 均支持流媒体播放。
-
为您的 SwiftUI App 添加多个窗口
了解最新的 SwiftUI API,以帮助您在 App 场景中显示窗口。我们将探索 MenuBarExtra 等场景类型可以如何帮助您借助 SwiftUI 轻松构建各种 App。我们还将向您介绍如何利用修饰符对 App 窗口的呈现和行为进行自定义,以进一步优化 macOS App。
资源
- Bringing multiple windows to your SwiftUI app
- DocumentGroup
- MenuBarExtra
- NewDocumentAction
- OpenDocumentAction
- OpenWindowAction
- Value and Reference Types
- Window
- WindowGroup
相关视频
WWDC22
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 大家好 我是 SwiftUI 团队的 工程师 Jeff 今天 我很高兴与您讨论 如何在 iPadOS 和 macOS 上 为您的 SwiftUI App 引入多个窗口 在本期视频中 我们将从 SwiftUI 生命周期中 各种场景类型的概览开始 其中包括我们 正在引入的一些新类型 接下来展示这些场景类型如何 通过添加辅助场景组合在一起 然后我们将介绍 一些用于在您的 App 中 为特定场景打开窗口的新 API 我们将用几种方法来 自定义 App 的场景 在深入研究一些新类型之前 我们先从现有场景类型的概览开始 您还记得在之前的视频中 SwiftUI 中的 App 是由场景和视图组成 场景通常用屏幕上的 窗口来展示其中的内容 例如 这是我构建的一个 App 我用它跟踪我正在阅读的书籍 我将其定义为单个窗口组 以适合平台的方式 显示我的阅读列表 在支持多个窗口的平台上 例如 iPadOS 和 macOS 可以使用多个窗口 来展示一个场景中的内容 场景的行为和表现形式 根据使用的类型不尽相同 例如 一个场景可能只用一个实例 便可展示自身内容 而无论平台能力如何 我们来看看 SwiftUI 中当前的 场景类型列表 WindowGroup 提供了 一种跨 Apple 平台构建 数据驱动 App 的方法 DocumentGroup 可让您在 iOS 和 macOS 上 构建基于文档的 App 并且 Settings 定义了 一个接口 用于表示 macOS 上的 App 内设置值 这些场景类型可以组合在一起 来扩展您 App 的功能 我们正在通过两个 新增内容扩展场景列表 第一个是 Window 一个代表所有平台上 单个 唯一窗口的场景 以及适用于 macOS 的 新场景类型:MenuBarExtra 在系统菜单栏中呈现为持久控件 与其他场景类型一样 您可以将 Window 和 MenuBarExtra 作为独立的场景使用 或与您 App 中的其他场景组合 与 WindowGroup 不同的是 Window 场景只会以单一 唯一的窗口实例展示其中的内容 此特性会很有用 体现在当您的场景 内容代表某个全局 App 状态 且不一定适合 macOS 和 iPadOS 上的 WindowGroups 多窗口演示样式 例如 游戏可能只允许 单个主窗口来呈现其内容 MenuBarExtra 是一种 新的 macOS 专用场景类型 其与其他场景 行为略有不同 MenuBarExtra 不会 在窗口中呈现其内容 而是将其标签放在菜单栏中 并将其内容显示在锚定到标签上的 菜单或窗口中 此外 只要其关联 App 正在运行 就可以使用 MenuBarExtra 无论该 App 是否位于最前端 MenuBarExtra 非常 适合创建可轻松访问其功能的 独立实用 App 或者可以与其他场景组合 提供访问 App 功能的替代方式 它还支持两种 渲染风格:一是默认风格 可在菜单中显示内容 改菜单从菜单栏下拉显示 以及在锚定到菜单栏的无镶边窗口中 显示其内容的风格 随着这两种新场景类型的加入 SwiftUI App 可以代表我们平台上 更丰富的功能集 让我们看看如何将这些新的 API 与我们现有的场景类型结合使用 这是我的 BookClub App 的定义 我之前展示过 其目前包含一个窗口组 在 macOS 上 我的 BookClub App 可能 受益于一个额外的窗口 来显示我们的阅读活动 这是一个很好的例子 其说明了 macOS App 如何利用 该平台上存在的 额外屏幕空间 和灵活的窗口安排 我们将在我们的 App 中添加一个辅助场景 用于表示此界面 我们的 Activity 窗口 数据来自 我们的整体 App 状态 所以窗口场景便是其理想选择 打开多个具有相同状态的窗口 不符合我们的设计 提供给我们场景的 标题将用作菜单项的标签 该菜单项已 添加到 Window 菜单部分 选择此项时 如果尚未打开场景的窗口 则会将其打开 否则 会将其带到前端 现在我们已经 介绍了将辅助场景添加到 我们的 BookClub App 我想介绍一些我们 即将推出的新的场景呈现 API 以及如何将其 集成到您的 App 中 来提供更丰富的体验 我们的 BookClub App 具有一个上下文菜单 可以为我们的 “内容列表”窗格中的任何书籍调用 此上下文菜单包含一个按钮 用于触发我们的窗口展示 稍后我会详解介绍 SwiftUI 通过环境 提供了几种 新的可调用类型 用于呈现 与您的 App 定义的 场景相关联的窗口 第一个是 openWindow 操作 可为 WindowGroup 或窗口场景呈现窗口 传递给操作的标识符必须匹配 您 App 中定义的场景的标识符 openWindow 操作 也可以取一个演示值 已呈现的场景将用该值显示其内容 这种形式的操作 只有使用新初始化程序的 WindowGroup 支持 稍后我将详细介绍 值的类型必须匹配 提供给场景初始化程序的类型 呈现文档窗口的环境中 还有两种可调用类型: 一是 newDocument 操作 支持为 FileDocuments 和 ReferenceFileDocuments 打开新的文档窗口 该操作需要您 App 中 相应的 DocumentGroup 定义为具有编辑角色 提供给此操作的文档将在 每次呈现窗口时创建 为了显示由磁盘上 现有文件提供内容的 文档窗口 可以使用 openDocument 操作 此操作需要一个 指向您要打开的文件的 URL 您的 App 必须 定义一个用于展示窗口的 DocumentGroup 并且该组的文档类型必须允许 在提供的 URL 处 读取文件的类型 回到我们的按钮 我们将在 我们的视图中添加 openWindow 环境属性 由于这种类型是可调用的 我们可以直接 从我们按钮的操作中调用 我们的“书籍”类型是可识别的 所以我们将其标识符 作为要呈现的值传递 在我们继续之前 我想介绍传递到 openWindow 操作的值 我注意到我正在传递该书籍的标识符 且该标识符是 UUID 类型的值 一般来说 您更倾向于 用上述方式使用 您模型的标识符 而不使用值本身 请注意 我们的 “书籍”类型是一个值类型 因此 如果我们将其用作呈现值 我们的新窗口将获得一份 原始演示的副本 对其中任何一个的编辑 都不会影响到另一个 使用书籍的标识符让我们的模型存储 成为这些值的真实来源 而不是通过为单个值提供多个绑定 有关值类型语义的更多信息 请参阅开发者文档 呈现的类型也必须符合 Hashable 和 Codable 协议 需要 Hashable 一致性 来将呈现的值 和打开的窗口关联 需要 Codable 一致性 来保持状态恢复的 呈现值 稍后 我将更详细地 介绍这两种行为 最后 如果可以的话 尽量倾向于传递轻量级值 我们书籍的标识符 也是一个很好的例子 由于 SwiftUI 将保留该值 以进行状态恢复 因此使用较小的值 将提高您 App 的响应速度 现在 我们的按钮具备了必要组件 来呈现我们的详细窗口 但是将其选择后不会显示任何内容 这是因为我们 已告知 SwiftUI 呈现 某种数据类型的窗口 但尚未在我们的 App 中 定义反映这一点的场景 我们回到我们的 App 并进行更改 除了我们主要的 WindowGroup 和辅助窗口 我们还将添加一个 用于处理我们书籍详情的 额外 WindowGroup 我们的书籍详情 WindowGroup 使用的是一个新的初始化程序 除了标题 我们还注意到该组 显示 Book.ID 类型的数据 在我们的例子则是 UUID 此类型应与我们传递到之前添加的 openWindow 操作的值 相匹配 当向 WindowGroup 提供 给定值用于演示时 SwiftUI 将为该值 创建一个新的子场景 并且该值将使用组的视图构建器 定义场景窗口的根内容 每个独特呈现的值将创造一个新场景 该值的等同性将用于确定 是否应该创建一个新窗口 或者是否可以重新使用现有窗口 当 openWindow 呈现一个 已存在窗口的值时 该组将使用该已存在窗口 而不会创建一个新窗口 以我们的 BookClub App 为例 为已经在窗口中呈现的书籍 选择上下文菜单操作 将导致该窗口被排在前面 而不是显示同一本书的第二个窗口 呈现的值也将由 SwiftUI 自动保留 以用于状态恢复 您的视图将被绑定到 初始呈现值 可在窗口打开时随时 修改此绑定 当重新创建场景以进行状态恢复时 SwiftUI 将 向窗口的内容视图 传递最新的值 在此 我们将 Book.ID 绑定到我们的详细视图 该视图可以查找我们模型存储中的 指定项以用于显示 随着我们所有的组件都准备到位 现在我们可以选择上下文菜单项 并在单独的窗口中查看书籍详情 最后 我想介绍一些 可以让您在 App 中 自定义场景的方法 由于我们用两个 WindowGroup 场景定义了我们的 App 一个用于主查看器窗口 另一个用于我们的详情窗口 所以 SwiftUI 会 默认为“文件”菜单中的 每个组添加菜单项 但是 我们详情窗口的菜单项 不太适合我们的用例 我更倾向于只能通过 之前添加的上下文菜单打开窗口 一个新的场景修改器 commandsRemoved 允许您修改场景 使其不再提供默认命令 比如“文件”菜单中的命令 应用此修改器后 我们的“文件”菜单现在只包含 用于为主要 WindowGroup 打开窗口的项 我对目前用于显示我阅读活动的 辅助窗口场景的演示还不太满意 那么 我们接下来仔细研究一下 由于我要对其应用一些修改器 所以我会将其提取到自定义场景中 这将使我的 App 定义更加清晰 没有任何以前的窗口状态 SwiftUI 将默认 将其放置在屏幕的中央 但是 我更倾向于将“阅读活动” 默认放置于不同的位置 通过添加新的 defaultPosition 修改器 当没有先前的状态可用时 我可以指定要使用的位置 该位置将适用屏幕大小 并按照当前的位置设置 将窗口放置在适当的位置 该新位置有助于将我的“活动”窗口 与屏幕上的其他查看窗口相区分 我还想让我的“活动”窗口 默认以特定大小显示 但仍可调整大小 除了 defaultPosition 我还将添加 defaultSize 修饰器 已向其提供的值将提供给布局系统 来导出窗口的初始大小 现在我已经自定义了窗口的呈现方式 我们再添加 一个修改器来自定义其行为 keyboardShortcut 修饰器 也已扩展为适用于场景类型 在场景级别使用时 此修改器影响创建 新窗口的命令 在此 我修改了我的“活动”窗口 以便可以使用 快捷键 Option-Command-0 将其打开 通过提供常用场景的快捷指令 可以很好地自定义 App 也可用于自定义 Command-N 的默认 快捷方式 且该快捷方式已添加到 App 的主 WindowGroup 中 介绍新场景 和 SwiftUI 中窗口功能 的旅途到此就结束了 我们认为 这些新 API 的潜力巨大 希望您也如此认为! 有关如何在 您的 iPadOS 和 macOS App 中 添加功能的更多信息 请查看其他视频 “iPad 上的 SwiftUI: 组织您的界面” 和“iPad 上的 SwiftUI: 添加工具栏、标题等”
感谢您的观看 ♪
-
-
2:01 - Scene composition
import SwiftUI import UniformTypeIdentifiers @main struct MultiSceneApp: App { var body: some Scene { WindowGroup { ContentView() } #if os(iOS) || os(macOS) DocumentGroup(viewing: CustomImageDocument.self) { file in ImageViewer(file.document) } #endif #if os(macOS) Settings { SettingsView() } #endif } } struct ContentView: View { var body: some View { Text("Content") } } struct ImageViewer: View { var document: CustomImageDocument init(_ document: CustomImageDocument) { self.document = document } var body: some View { Text("Image") } } struct SettingsView: View { var body: some View { Text("Settings") } } struct CustomImageDocument: FileDocument { var data: Data static var readableContentTypes: [UTType] { [UTType.image] } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } self.data = data } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { FileWrapper(regularFileWithContents: data) } }
-
2:34 - Adding a window scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
3:01 - Standalone menu bar extra app
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Utility App", systemImage: "hammer") { AppMenu() } } } struct AppMenu: View { var body: some View { Text("App Menu Item") } }
-
3:35 - Windowed app with menu bar extra
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) MenuBarExtra("Book Club", systemImage: "book") { AppMenu() } #endif } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct AppMenu: View { var body: some View { Text("App Menu Item") } } class ReadingListStore: ObservableObject { }
-
3:42 - Menu bar extra with default style
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Utility App", systemImage: "hammer") { AppMenu() } } } struct AppMenu: View { var body: some View { Text("App Menu Item") } }
-
3:49 - Menu bar extra with window style
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Time Tracker", systemImage: "rectangle.stack.fill") { TimeTrackerChart() } .menuBarExtraStyle(.window) } } struct TimeTrackerChart: View { var body: some View { Text("Time Tracker Chart") } }
-
4:14 - Book Club app definition
import SwiftUI @main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } class ReadingListStore: ObservableObject { }
-
4:38 - Adding an auxiliary Window Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
5:28 - Open book context menu button
import SwiftUI struct OpenBookButton: View { var book: Book var body: some View { Button("Open In New Window") { } } } struct Book: Identifiable { var id: UUID }
-
5:34 - Opening a window using an identifier
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct OpenWindowButton: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Activity Window") { openWindow(id: "activity") } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
5:57 - Opening a window using a presented value
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
6:16 - Opening a window with a new document
import SwiftUI import UniformTypeIdentifiers @main struct TextFileApp: App { var body: some Scene { DocumentGroup(viewing: TextFile.self) { file in TextEditor(text: file.$document.text) } } } struct NewDocumentButton: View { @Environment(\.newDocument) private var newDocument var body: some View { Button("Open New Document") { newDocument(TextFile()) } } } struct TextFile: FileDocument { var text: String static var readableContentTypes: [UTType] { [UTType.plainText] } init() { text = "" } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } text = string } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = text.data(using: .utf8)! return FileWrapper(regularFileWithContents: data) } }
-
6:41 - Opening a window with an existing document
import SwiftUI import UniformTypeIdentifiers @main struct TextFileApp: App { var body: some Scene { DocumentGroup(viewing: TextFile.self) { file in TextEditor(text: file.$document.text) } } } struct OpenDocumentButton: View { var documentURL: URL @Environment(\.openDocument) private var openDocument var body: some View { Button("Open Document") { Task { do { try await openDocument(at: documentURL) } catch { // Handle error } } } } } struct TextFile: FileDocument { var text: String static var readableContentTypes: [UTType] { [UTType.plainText] } init() { text = "" } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } text = string } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = text.data(using: .utf8)! return FileWrapper(regularFileWithContents: data) } }
-
7:03 - Book details context menu button
struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct Book: Identifiable { var id: UUID }
-
7:08 - Book details context menu button
struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct Book: Identifiable { var id: UUID }
-
9:06 - Book Club app with book details Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
10:32 - Book Club app with book details Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
11:16 - Removing default commands for the book details scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } .commandsRemoved() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
11:46 - Extracting reading activity into custom scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } ReadingActivityScene(store: store) WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } .commandsRemoved() } } struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
12:04 - Applying the defaultPosition modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } .defaultPosition(.topTrailing) } } class ReadingListStore: ObservableObject { }
-
12:32 - Applying the defaultSize modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } #if os(macOS) .defaultPosition(.topTrailing) .defaultSize(width: 400, height: 800) #endif } } class ReadingListStore: ObservableObject { }
-
12:50 - Applying the keyboardShortcut modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } #if os(macOS) .defaultPosition(.topTrailing) .defaultSize(width: 400, height: 800) #endif #if os(macOS) || os(iOS) .keyboardShortcut("0", modifiers: [.option, .command]) #endif } } class ReadingListStore: ObservableObject { }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。