大多数浏览器和
Developer App 均支持流媒体播放。
-
在 SwiftUI 中设计窗口
了解如何在 visionOS、macOS 和 iPadOS 中打造出色的单窗口和多窗口 App。探索相关工具,助你以编程方式打开和关闭窗口、调整窗口位置和大小,甚至替换窗口。我们还将探索窗口设计原则,帮助用户在他们的工作流程中使用你的 App。
章节
- 0:00 - Introduction
- 1:19 - Fundamentals
- 6:15 - Placement
- 9:43 - Sizing
- 12:03 - Next steps
资源
相关视频
WWDC24
WWDC23
-
下载
大家好! 我是 SwiftUI 团队的 Andrew 很高兴能和大家聊聊 SwiftUI App 中的窗口设计
窗口是 App 内容的容器
通过窗口 用户可以使用熟悉的控件 管理 App 的各个部分
比如能够调整位置
调整尺寸
或关闭窗口
我将使用 BOT-anist 这是一款 我和朋友一起开发的 SwiftUI App
这是模拟器中的 BOT-anist 机器人编辑器 你可以在这里自定机器人
玩家可以将这个机器人带入游戏中 帮助机器人照料植物
BOT-anist 可针对 iOS、iPadOS、 visionOS 和 macOS 提供个性化体验
我将介绍的概念适用于多窗口平台 但在这个视频中 我将重点介绍 visionOS
我将介绍如何定义、打开和使用窗口
如何控制窗口的初始位置 以及调整窗口尺寸的不同方法
首先介绍一下基础知识
通过单独的窗口 用户可以同时使用 App 的不同部分
而且 拥有同一界面的不同实例 能发挥强大的作用
用户可以使用系统控件 单独操作每个窗口
比如能够调整窗口尺寸、 调整位置或缩放大小
而且 每个窗口都可利用 平台特定功能的优势 例如在 visionOS 上 通过使用视体窗口样式 可在窗口中包含 3D 内容
虽然多窗口功能很强大 但使用 TabView 等 单个顶层视图可以简化体验
要进一步了解 TabView 和 其他顶层视图 请观看 “将你的窗口 App 提升至 空间计算领域”
要了解在 visionOS 中何时适合使用 多个窗口 请观看 “设计空间用户界面”
BOT-anist 在 visionOS 中 有两个主要场景
编辑器窗口和游戏空间容器
每个场景都由 WindowGroup 定义
这个 App 打开机器人编辑器 WindowGroup 的一个实例
窗口中的一个按钮可打开 “game”WindowGroup 实例
视体窗口样式使得这个窗口 在 visionOS 中成为一个空间容器
我想为 BOT-anist 添加两项新功能
第一项功能将打开一个新窗口 其中包含一部关于机器人的影片
影片是一个包含在门户中的 3D 场景
在 App 主体中 我添加了一个包含 3D 场景视图的新 WindowGroup 为了标识这个 WindowGroup 我将它的 ID 设为“movie” 我将使用这一 ID 来打开窗口
我会将这个 ID 传递给某个环境操作 这些操作可用于 SwiftUI 层次结构中的任意位置 几个不同的环境操作可用于管理窗口
使用 openWindow 可打开窗口
使用 dismissWindow 可关闭窗口
使用 pushWindow 可打开窗口 并隐藏原始窗口
我将使用 openWindow 打开新的影片窗口
在机器人编辑器视图中 我将使用 openWindow 的键路径创建环境属性 以从环境中检索 OpenWindowAction 然后 我可以在新按钮中 执行 OpenWindowAction 传入我刚才为窗口组 定义的 ID“movie”
现在 轻点编辑器中的相应按钮 就会以独立窗口的形式打开影片门户
我觉得 编辑器不应该与影片视图同时显示 因此 我将使用 pushWindow 环境操作来显示窗口
这将打开新窗口而不是原始窗口
如果关闭新窗口 原始窗口将重新出现
为了在打开影片窗口时隐藏编辑器 我将环境属性键路径 从 openWindow 改为 pushWindow 然后更新按钮以调用这一操作
现在 轻点 TV 按钮将推出影片窗口 并将隐藏机器人编辑器窗口
现在我就可以认真观看 我的机器人影片内容了 而不用担心受到干扰
轻点关闭按钮可返回编辑器 无需额外逻辑即可实现这一行为 如果显示的内容不需要 与当前展示窗口同时显示 请考虑使用这个操作
定义并打开窗口后 现在可以借助平台特定功能 更自如地改善窗口效果 比如 “无边记”使用 工具栏装饰元素 沿窗口底部边缘显示控件
或是 ToolbarTitleMenu 在不挤占空间的情况下 呈现与文档相关的操作
默认情况下 窗口栏和 关闭按钮始终可见 但是 对于影片视图 我使用了 .persistentSystemOverlays 修饰符进行隐藏 以便用户更专注地观看影片
这些 API 非常适合用来改善 visionOS 窗口效果
要了解如何在 macOS 中优化窗口 请观看 “利用 SwiftUI 量身定制 macOS 窗口”
影片窗口看起来棒极了! 接下来 我想为游戏添加 一个可选控制面板 这个面板将提供用于 移动机器人的额外控件 以及一些用于执行跳跃 或挥手等动作的按钮
我已经添加了一个 显示这些控件的新窗口组
以及游戏空间容器中的 openWindow 调用
现在 轻点游戏中的相应按钮 就会在一个新窗口中打开控件
这些控件可以移动位置 而不受游戏空间容器的影响
但是 在首次打开窗口时 它会遮挡空间容器 而且显示位置可能比较远
与控制面板一样 visionOS 打开的新窗口 位于原始窗口前方
而 macOS 打开的新窗口 则位于屏幕中央
这一行为可通过 defaultWindowPlacement 修饰符进行自定
我们可以通过编程方式 来设置窗口的初始位置和尺寸
根据平台的不同 用户可以通过 多种方式调整窗口的位置和尺寸
可以根据其他窗口调整位置 比如 放在其他窗口的左侧或右侧
根据玩家调整 比如在 visionOS 中 应用 utilityPanel 这样窗口就会靠近玩家 并且通常在可直接触摸的范围内
还可以根据屏幕调整 比如 在 macOS 中显示在屏幕右上角
要让游戏控件在 visionOS 中 看起来靠近玩家
我对“controller”窗口组应用了 defaultWindowPlacement 修饰符
在此基础上 我返回了位置为 .utilityPanel 的 WindowPlacement
并对返回值添加了 if 条件 以确保这一位置仅适用于 visionOS
这样一来 窗口首次打开时 控件就会显示在玩家附近 玩家还可以根据需要将窗口 从初始位置移动到其他位置
使用这些新控件 我可以通过全新的方式 与机器人互动
比如 轻点这个按钮 让 BOT-anist 挥手
visionOS 中的控制器窗口 就设置好了! 接下来 在 macOS 中 我将手动计算这个窗口的位置
defaultWindowPlacement 修饰符会提供上下文 根据平台的不同 这将包含不同的信息 在 macOS 中 上下文包含 与默认显示有关的信息 通过访问 我得到了 .visibleRect 这表示这个位置可以安全地放置内容
使用 sizeThatFits 方法时 我询问窗口有哪些内容、 需要设置什么样的尺寸
使用 displayBounds 和尺寸变量 我计算出的位置正好是 显示屏底部正上方 且水平居中的位置
现在 我可以返回已计算出位置 和尺寸的 WindowPlacement 了
现在 我的控件也就自然地 显示在 macOS 上了
玩游戏时 玩家可以 自由调整窗口的位置 甚至可以将它放在单独的屏幕中
我太喜欢这些 新的窗口放置功能了 为了确保我的内容 始终呈现出最佳状态 我还想更改调整窗口尺寸的方式
窗口的初始尺寸由系统决定 你可以通过几种不同的方式 来更改默认尺寸
如果尺寸取决于屏幕尺寸或其他窗口 则你可以通过默认的窗口放置 API 来指定初始尺寸 这就跟我在 macOS 中 对控制器窗口的设置一样
或者你可以使用 defaultSize 修饰符来更改初始尺寸
请注意 如果还有其他尺寸约束 比如窗口放置 API 提供的尺寸 或恢复场景时的设置 则不会应用这个默认尺寸
对于推出的窗口 比如我之前添加的影片窗口 defaultSize 与原始窗口的尺寸相同 在本例中 原始窗口是机器人编辑器
我对默认尺寸很满意 不过玩家可能想要 调整影片窗口的尺寸 为了不影响影片的观看效果 我会设定一些限制
通过指定 “movie”WindowGroup 应该有一个具有 .contentSize 的 .windowResizability 窗口尺寸将被限制在它所包含内容的 最小和最大尺寸范围内
在影片内容视图中 我添加了 宽度和高度的最大值和最小值
现在 影片窗口可以调整为小正方形 也可在合理范围内调大尺寸
这个 BOT-anist 我能盯着看一整天! 不过 我们还是把注意力 放到控件窗口上
可以将这个窗口的尺寸调整得非常大 大到会挡住空间容器
但最好是让这个窗口的尺寸 与它包含的内容尺寸相匹配
就像我对影片 WindowGroup 所做的设置那样 我还在控制器窗口组添加了 windowResizability 修饰符
现在 当我更改控制器模式时 窗口就会自动调整大小 以匹配所显示内容的尺寸
请注意 玩家无法调整 这个窗口的尺寸 因为每种模式所对应的视图 都有固定的尺寸 而不是最小和最大尺寸
BOT-anist 的开发进展顺利 我已经针对这个 App 的 visionOS 和 macOS 版本做了一些优化 你的 App 也能充分利用 窗口功能及支持的 API
思考一下你的 App 是更适合窗口 还是顶层视图 使用窗口放置 API 以提供初始布局 根据所含内容调整窗口尺寸 并设置窗口尺寸调整限制
并善加利用平台特定的窗口功能 从而让你的 App 用起来更舒服
感谢大家的参与!希望大家能够 在 App 中自如地运用窗口功能
-
-
2:36 - BOT-anist scenes
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) } }
-
3:09 - Creating the movie WindowGroup
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) WindowGroup(id: "movie") { MovieContentView() } } }
-
3:55 - Opening a movie window
struct EditorContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Movie", systemImage: "tv") { openWindow(id: "movie") } } }
-
4:45 - Pushing a movie window
struct EditorContentView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Open Movie", systemImage: "tv") { pushWindow(id: "movie") } } }
-
5:34 - Toolbar
CanvasView() .toolbar { ToolbarItem { Button(...) } ... }
-
5:40 - Title menu
CanvasView() .toolbar { ToolbarTitleMenu { Button(...) } ... }
-
5:48 - Hiding window controls
WindowGroup(id: "movie") { ... } .persistentSystemOverlays(.hidden)
-
6:28 - Creating the controller window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } WindowGroup(id: "controller") { ControllerContentView() } } }
-
6:34 - Opening the controller window
struct GameContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { ... Button("Open Controller", systemImage: "gamecontroller.fill") { openWindow(id: "controller") } } }
-
7:46 - Positioning the controller window
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) ... #endif }
-
8:45 - Positioning the controller window continued
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) let displayBounds = context.defaultDisplay.visibleRect let size = content.sizeThatFits(.unspecified) let position = CGPoint( x: displayBounds.midX - (size.width / 2), y: displayBounds.maxY - size.height - 20 ) return WindowPlacement(position, size: size) #endif }
-
10:12 - Default size
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } .defaultSize(width: 1166, height: 680) } }
-
10:49 - Setting resize limits on the movie window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() .frame( minWidth: 680, maxWidth: 2720, minHeight: 680, maxHeight: 1020 ) } .windowResizability(.contentSize) } }
-
11:37 - Controller window resizability
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "controller") { ControllerContentView() } .windowResizability(.contentSize) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。