关于 SwiftUI Multiplatform Design Guide
这是一份面向 Apple 平台的 SwiftUI 多平台设计指南,讨论如何针对不同设备调整场景、状态和交互,而非只追求代码复用。
主要功能
- 01场景与窗口架构
- 02按窗口隔离状态
- 03基于文档的工作流
- 04ImmersiveSpace 转场
- 05跨平台输入与布局
适用场景
- 01规划 iOS 与 macOS 应用
- 02隔离多个窗口的状态
- 03设计文档类效率应用
- 04处理 visionOS 沉浸空间转场
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mosif16/ codex-skills --skill swiftui-multiplatform-design-guide安装后在 Claude Code 中输入/swiftui-multiplatform-design-guide确认已加载。
详细介绍
SwiftUI 中的统一架构:面向 iOS、iPadOS、macOS 和 visionOS 的全面设计策略
1. 引言:空间计算与平面计算的融合
当代 Apple 软件开发格局的特点,是交互范式的彻底融合。过去,不同平台之间界限分明:iOS 以触控目标和对玻璃屏幕的直接操作为核心;macOS 则依赖精准的光标和鼠标带来的间接抽象;iPadOS 处于两者之间,试图弥合差异。然而,visionOS 的推出从根本上打破了这种分层架构。它催生了一种全新的设计语言,不仅要适应屏幕尺寸,还要适应物理维度、输入方式和用户意图。
本文将全面分析如何使用 SwiftUI 构建真正的多平台应用所需的架构策略。我们不再停留于表面的“跨平台”概念——它往往等同于采用“最低公分母”方案——而是提倡采用统一的状态驱动架构,同时尊重各类设备独特的人体工学特征。本文将探讨如今支撑 Apple 视觉语言的“Liquid Glass”隐喻、复杂的场景与窗口层级,以及统一的输入模型;借助这些模型,同一语义操作既可以通过轻点、单击,也可以通过注视触发的捏合手势来执行。
1.1 人机界面指南的演变
Apple 人机界面指南(HIG)已从面向不同平台的静态规则集合,演变为流动而统一的设计理念。这一理念的核心支柱——空间、沉浸感、透视和空间音频——不再仅属于头显。^1^ 它们体现了向“空间计算”的转变,这种转变甚至会影响平面界面。例如,visionOS 图标中的分层和深度效果,在理念上与 tvOS 的视差效果和 iOS 的流畅动画密切相关。^2^
这场演变带来的一个重要启示,是“透视式”设计的概念:它不仅指光学层面上看见现实世界,也指在界面层面尽量减少遮挡。在 iOS 上,这体现为半透明材质和工作表;在 visionOS 上,则是字面意义上的透明。要实现这一点,需要采用“内容优先”的方法,让“界面装饰”(界面元素)退居其次。HIG 明确建议:以窗口承载以 UI 为核心的体验,仅在特定时刻采用沉浸式体验;这一模式与 iOS 上的模态呈现如出一辙。^1^
1.2 App 图标在多平台身份中的作用
作为进入任何应用的入口,App 图标是多平台适配的第一课。图标常被视为普通素材而遭忽视,但它的表现方式会左右用户的预期。在 iOS 上,图标是扁平化的超椭圆形,会随主屏幕布局变化而调整。在 macOS 上,图标则采用统一的“方糖”造型,并带有投影,以体现桌面隐喻。然而,在 tvOS 和 visionOS 上,图标变成了分层的三维对象。
对这类图标构成方式的研究揭示了一种“Liquid Glass”美学:各层融合在一起,营造出立体感。在 visionOS 上,图标通常由一个背景层和一到两个前景层组成。当用户注视图标时,系统会应用镜面高光和视差效果,将前景层抬起,使其仿佛迎向用户的视线。^2^ 这不仅是装饰效果,也是在为用户提供入门引导,让他们明白“注视”本身就是一种交互行为。因此,有效的多平台架构必须将这种分层思维延伸到应用本身:将 UI 元素视为层级中悬浮的平面,而不是平面的像素,并让它们能够响应眼睛或指针的“焦点”。
2. 场景层级:窗口、文稿和空间
在 SwiftUI 生命周期中,Scene 协议已取代 View,成为应用结构的基本单元。View 定义用户看到的内容,而 Scene 定义操作系统如何管理这些内容的存在、持久性和生命周期。熟练掌握 Scene 类型——特别是 WindowGroup、DocumentGroup 和 ImmersiveSpace——是构建能够从 iPhone SE 扩展到 Vision Pro 的应用的前提。
2.1 WindowGroup 范式与生命周期管理
WindowGroup 是 SwiftUI 的主力。它为应用的主要界面定义了一个模板。不过,它的行为会因运行平台而发生显著变化。在 iPhone 上,它通常会实例化一个持久窗口。在 iPadOS 和 macOS 上,它则充当工厂,用于创建多个并发窗口。^4^
这种多态性给状态管理带来了显著的架构复杂性。早期 SwiftUI 开发中的一种常见反模式,是依赖在 App 层级注入的单例 AppState 对象。虽然这种架构在单窗口 iPhone 上可行,但在 iPadOS 和 macOS 上却会失效。如果用户打开同一应用的两个窗口来对比数据,单例架构会迫使两个窗口共享相同的导航路径和选择状态。在窗口 A 中导航会意外地驱动窗口 B 的导航,从而破坏用户对独立工作区的认知。
为解决这个问题,现代多平台架构要求将状态限定在 Scene 范围内。每当 WindowGroup 实例化一个新窗口时,都必须为该窗口的视图层级创建一套全新的依赖关系图。这通常需要创建一个每窗口独立的 ViewModel 或 NavigationManager,并将其注入窗口根视图的环境,而不是应用的根视图环境。^6^
2.1.1 窗口可调整大小性与几何属性
“窗口”的概念因物理环境而异。在 macOS 上,窗口是由屏幕边框限定的矩形,用户可以完全调整其大小。在 visionOS 上,窗口是漂浮在 3D 空间中的平面,同样可以调整大小,但会受到可读性和视野范围的限制。
SwiftUI 提供了 .windowResizability 和 .defaultSize 修饰符来控制这一行为。
-
内容大小策略: 使用 .windowResizability(.contentSize) 可让窗口紧贴 SwiftUI 内容调整大小。这非常适合设置面板或实用工具对话框。^4^
-
自动策略: 由系统决定大小。在 visionOS 上,这会受到用户距离的影响。远处看起来“较小”(角度尺寸较小)的窗口,实际渲染的像素分辨率可能与近处看起来“较大”的窗口相同。系统会自动缩放内容以保持可读性(动态字体),这意味着开发者必须按“点”而非像素进行布局,以避免空间环境中的文字过小。^1^
2.2 DocumentGroup 与基于文件的工作流
对于生产力应用,DocumentGroup 场景类型不可或缺。它开箱即用地集成了平台的文件系统(macOS 上的 Finder,iOS/iPadOS 上的“文件”)。更重要的是,它自动处理桌面级应用所需的复杂“脏状态”管理和文件锁定机制。^8^
部署 DocumentGroup 后,SwiftUI 会自动将应用的文档模型(遵循 FileDocument)桥接到系统的标准菜单命令。在 macOS 上,“文件 > 打开”、“文件 > 保存”和“文件 > 复制”菜单项会自动连接到当前活动的文档场景。这是 SwiftUI 的一项关键“免费”功能,可替代数百行 AppKit 样板代码。
不过,将标准应用改为 DocumentGroup 应用,需要从根本上改变数据流。数据的真实来源变成磁盘上的文件,而非云数据库或本地缓存。这对 visionOS 有一定影响,因为文件操作可能会与其他应用在共享空间中同时进行。在 visionOS 上,DocumentGroup 会将文件显示为浮动窗口,用户可以在周围的空中排列多个文档(例如 PDF 或电子表格),实际上将物理房间变成多显示器桌面。^8^
2.3 ImmersiveSpaces:虚拟现实的独占性
ImmersiveSpace 场景类型代表了统一架构中最显著的分歧。窗口是内容的容器,而 ImmersiveSpace 则允许应用成为整个环境的容器。
ImmersiveSpace 的关键架构限制是独占性。在 visionOS 上,多个应用可以在“共享空间”中打开窗口,但任意时刻只能有一个应用占据“沉浸空间”。如果应用 A 处于沉浸状态,应用 B 的沉浸内容就会隐藏。因此,必须严格管理场景切换逻辑。打开沉浸空间是一项异步操作(openImmersiveSpace),可能失败,也可能被系统取消。^10^
2.3.1 “加载窗口”切换模式
在不同沉浸式环境之间切换(例如,从“火星”环境切换到“月球”环境)时,会遇到一个特殊难题。由于一次只能打开一个空间,应用必须先关闭当前空间,再打开下一个空间。不过,如果应用在打开窗口之前就关闭当前空间,应用实际上就没有任何打开的场景了。在 visionOS 中,没有任何打开场景的应用会被系统终止。
为解决这个问题,经验丰富的开发者会采用“加载窗口”模式。
-
触发: 用户选择新环境。
-
操作: 应用打开一个小型临时窗口(例如显示“正在加载...”的转圈指示器)。
-
同步: 确认窗口可见后(通过 scenePhase 或 onAppear),应用关闭当前 ImmersiveSpace。
-
切换: 应用请求打开新的 ImmersiveSpace。
-
清理: 新空间成功呈现后,应用关闭加载窗口。
这种“先建后拆”的逻辑对于防止 VR 环境中大量上下文切换时应用崩溃至关重要。^12^ 它凸显了处理异步场景拓扑时状态管理的复杂性,而 iOS 的同步导航栈中不存在这个问题。
2.4 ScenePhase 状态机
通过 ScenePhase 观察这些场景的生命周期。这个环境值会报告场景处于 .active、.inactive 还是 .background 状态。^13^
-
**活跃:**场景正在接收输入事件。
-
**非活跃:**场景可见,但未获得焦点。在 macOS 上,这是一个后台窗口。在 iPadOS 上,这是分屏视图中不可交互的一侧。在 visionOS 上,用户将视线移开某个窗口或与共享空间中的其他应用交互时,通常会进入此状态。
-
**后台:**场景不可见。
处理 .inactive 状态对电池续航和性能至关重要,在 visionOS 上尤其如此。如果应用在体积窗口中运行耗费资源的粒子模拟,那么在进入 .inactive 状态(即用户移开视线)时,应立即暂停该模拟。否则,GPU 会在用户无法感知的内容上浪费大量资源。^14^
表 1:各平台的场景功能与限制
功能 iOS iPadOS macOS visionOS
并发性 单个场景 多个窗口 多个 多个 (大多如此) 窗口 窗口和 体积空间
窗口 仅系统控制 分屏视图 / 台前调度 用户 用户(统一 调整大小 管理器 (自由布局) 缩放)
坐标 2D 屏幕 2D 屏幕 2D 屏幕 3D 无限 空间 (共享空间)
沉浸式 不适用 不适用 不适用 独占(仅限 模式 单个应用)
终止 转入后台时 滑动关闭/转入后台时 按下 Cmd+Q / 所有场景关闭时 时 最后一个窗口关闭
3. 自适应布局策略:从侧边栏到装饰元素
从 5 英寸 iPhone 到 Vision Pro 的无限画布,屏幕空间的巨大差异要求布局策略不仅要能响应(拉伸内容),还要具备自适应能力(改变层级结构)。过去事事都用 NavigationSplitView 的做法已被证明无法满足复杂的顶层导航需求,因此开始采用新的 sidebarAdaptable 范式。
3.1 NavigationSplitView 不再适用于顶层导航
多年来,NavigationSplitView(及其前身 NavigationView)一直是“主从”界面的标准方案。它提供侧边栏、内容栏和详情区域。在 iPad 和 macOS 上,这种方案适用于浏览层级数据(例如“邮件”和“备忘录”)。
然而,对于具有不同且互不相关功能区域的应用(例如包含“资料库”“电台”和“搜索”的音乐应用),NavigationSplitView 不适合作为根导航容器。使用分屏视图会迫使这些不同区域汇入同一个列表,这在移动设备上往往显得过于简单。此外,在 visionOS 上,NavigationSplitView 会使用特定的玻璃材质渲染,侧边栏会略微后移至 Z 空间。虽然视觉效果出众,但嵌套分屏视图(例如在标签视图中放置分屏视图)可能造成视觉杂乱;在某些版本的 macOS 上,还可能因工具栏冲突导致应用崩溃。^15^
3.2 SidebarAdaptable 革命
随着 iOS 18 和 macOS 15 的推出,Apple 为 TabView 引入了 .tabViewStyle(.sidebarAdaptable) 修饰符。这代表着布局架构的根本性转变, 有效地将标签栏和侧边栏的概念合并为一个灵活的 组件。^17^
此修饰符允许标准 TabView 根据水平尺寸类别调整其呈现模式:
-
紧凑宽度(iPhone/iPad Slide Over): 呈现为 传统的底部标签栏。
-
常规宽度(iPad 全屏): 呈现为顶部标签栏,允许用户切换到侧边栏。
-
macOS: 自动呈现为侧边栏,并采用原生的半透明材质。
-
visionOS: 转变为一个垂直的“装饰”元素,悬浮在 主窗口左侧。^17^
这种统一解决了“侧边栏与标签栏”代码分叉的问题。 开发者不再需要编写 if os(iOS) { TabView } else { NavigationSplitView }。他们可以编写一个 TabView,并相信 系统会呈现符合人体工学的容器。
3.2.1 用于分层展示的 TabSection
sidebarAdaptable 的一个重要搭档是 TabSection。在底部 标签栏中,空间十分宝贵;通常只能容纳 3-5 个项目。而在侧边栏中,垂直滚动 可以容纳数十个项目。TabSection 允许开发者对标签进行逻辑分组。^19^
-
iOS 上的行为: TabSection 标题通常会被折叠或 忽略,以确保底部栏保持简洁。
-
macOS/iPad(侧边栏)上的行为: TabSection 标题会显示为 独立的可折叠标题(例如,“资料库”、“播放列表”、 “设置”)。
这使“密度自适应”设计成为可能。应用可以在侧边栏模式下展示细化的 导航选项(例如特定文件夹),同时只需通过 TabSection 构建层级结构, 就能在标签栏模式下将它们收纳到一个“浏览”标签中。^20^
3.3 深层导航状态保留
自适应布局中最棘手的挑战之一,是在布局容器发生变化时保留 导航状态。如果用户在 iPad 上的导航堆栈中深入操作(处于侧边栏模式), 然后与另一个应用创建分屏视图,iPad 可能会缩窄到紧凑 宽度,迫使应用切换到底部标签栏。
如果导航状态存储在视图层级中(例如, 视图内部的 @State),此转换会销毁状态,使用户回到根视图。
架构解决方案: 必须将导航状态从视图中提取出来, 放入模型或环境中。通过使用 NavigationStack(path: $navigationModel.path),导航堆栈的路径 便与 UI 容器解耦。当 TabView 从侧边栏变为标签栏时,新的 NavigationStack 实例会绑定到 同一个持久路径对象,让用户能够无缝“回到”转换前正在查看的 同一个详情屏幕。^21^
3.4 使用 ViewThatFits 实现微布局
sidebarAdaptable 负责处理宏观结构,而单个单元格和面板的布局则需要 ViewThatFits。这个容器本质上是一个布局逻辑门:它接收一个视图闭包,并渲染其中第一个能在可用空间内完整显示、不会被截断的视图。^23^
对于 visionOS,这实际上是必不可少的。与尺寸固定的 iOS 设备不同,visionOS 窗口几乎可以调整为任意宽高比。在 1000pt 宽度下效果很好的水平卡片布局,在 400pt 宽度下可能会显得很糟。ViewThatFits 允许开发者为同一个组件提供「水平」和「垂直」两个版本。
Swift
ViewThatFits(in:.horizontal) {
HStack { /* Wide layout */ }
VStack { /* Tall layout */ }
}
这种声明式方法取代了过去脆弱的 GeometryReader 模式,后者经常导致布局循环和性能下降。^23^ 它确保应用的内部组件与外部容器一样灵活。
4. 输入模型:三模态交互系统
平台之间最根本的区别,在于用户表达意图的方式。「三模态」系统涵盖直接操作(触摸)、间接指向(鼠标/触控板)和基于意图的定位(注视/捏合)。统一的架构必须将这些物理差异抽象为语义操作。
4.1 统一点击与空间交互
在触控界面发展的早期,TapGesture 很简单:它表示 2D 数字化仪上的一个坐标。visionOS 让事情变得复杂了。在 visionOS 上,「点击」实际上是一个复杂的操作序列:用户的眼睛对准一个实体(悬停),然后用手指捏合(确认)。这个点击的「位置」不在屏幕上,而是眼睛注视射线与虚拟物体碰撞体的交点。^25^
SwiftUI 通过 SpatialTapGesture 将两者统一起来。
-
在 iOS/macOS 上: 它会报告点击/轻点相对于视图的 2D 位置。
-
在 visionOS 上: 它会报告射线投射与实体表面交点的 3D 位置。
这对于交互式 3D 内容至关重要。例如,在一款立体太阳系应用中,SpatialTapGesture 允许用户轻点 3D 月球模型上的某个特定陨石坑。处理该操作的代码可以在各个平台间共享;iOS 版本只需将触摸坐标解释为 3D 视图平面投影上的坐标即可。^26^
4.2 目光与悬停效果的原理
在 visionOS 上,“光标”就是用户的眼睛。然而,与鼠标光标不同,眼球会抖动,且眼动是不自觉的。为防止意外触发(即“点石成金”问题),visionOS 不会向应用报告注视坐标,只有在用户作出选择后才会报告。
系统会改用 悬停效果 提供反馈。当用户注视某个交互元素时,系统会将其高亮显示。这种反馈对于帮助用户发现可交互元素至关重要。在 visionOS 上,没有悬停效果的按钮会显得“毫无反应”,用户会以为它是静态文本。
架构要求: 多平台应用中的每个自定义交互视图都必须应用 .hoverEffect() 修饰符。
-
iPadOS: 添加指针吸附效果和形状变形效果。
-
visionOS: 添加半透明光晕或“抬起”效果。
-
macOS: 通常会忽略此效果,或将其映射为标准光标变化。
Apple 推出的 CustomHoverEffect 协议(iOS 18+)支持精细控制。开发者可以精确定义视图在获得焦点时如何缩放、变暗或生成阴影。这让由指针驱动的 Mac 和由目光驱动的头戴设备能够统一采用“焦点”的视觉语言。^28^
隐私影响: 由于应用无法获知用户正在注视何处,因此依赖“悬停以显示”的机制(例如工具提示)由系统处理,或需要特定手势(例如长时间注视)。开发者不能仅根据注视停留时间以编程方式触发逻辑,必须有用户的明确交互。^30^
4.3 间接输入:焦点引擎
在 iOS 上,触控是主要的交互方式;而在 macOS 上,键盘交互是主要方式,在 visionOS 上(对于生产力场景)也可以说与目光交互同样重要。FocusState API 是实现这一点的桥梁。
正确实现 FocusState,可确保用户能够通过 Tab 键浏览应用。这不仅是 macOS 的功能;将蓝牙键盘连接到 Vision Pro 后,也可以使用相同的 Tab 键导航。如果应用只依赖 onTapGesture,就无法供键盘用户访问。使用 Button 和 FocusState,可确保“焦点环”(macOS)或“焦点高亮”(visionOS)在 UI 中按逻辑顺序移动。^31^
4.4 手势组合与冲突
多平台应用经常会遇到手势冲突。在 iOS 上,List 处理垂直滚动;在 visionOS 上,List 使用“捏合并拖动”来滚动。如果开发者在列表行上添加自定义 DragGesture(例如用于“滑动删除”或自定义滑块),就可能阻止 visionOS 上的滚动手势。
最佳实践:
-
使用标准组件: 标准 List 滑动操作会自动在所有平台上生效。
-
针对性手势: 在 visionOS 中使用 RealityView 时,请使用 .gesture(MyGesture().targetedToEntity(myEntity))。这样可将手势限定在特定的 3D 对象上,避免手势拦截窗口级滑动操作。^27^
-
同时识别: 谨慎使用 .simultaneousGesture,但当滚动视图中的按钮需要在滚动视图识别到拖动开始之前识别点击时,通常有必要使用它。^33^
5. 命令与控制基础设施
以工具栏为主的 iOS 与以菜单为主的 macOS 之间的差异,是造成使用阻力的主要原因。真正原生的 macOS 应用依靠菜单栏帮助用户发现命令,而 iOS 则依靠可见按钮。
5.1 重新构想响应者链:FocusedValue
SwiftUI 通过 FocusedValue 和 Commands 重新引入了“响应者链”的概念。这使 触发操作(菜单项)与 执行操作(视图逻辑)得以清晰分离。
以“删除”命令为例。
-
触发操作: 菜单栏“编辑”菜单中的“删除”项。
-
执行操作: 特定视图中的一个函数,用于删除选中的项目。
在多窗口应用中,菜单栏是全局的。它如何知道要删除哪个窗口中的内容?
解决方案是 FocusedValue。
-
视图定义一个遵循 FocusedValueKey 的结构体。
-
视图将 .focusedValue(\.deleteAction, deleteFunction) 附加到自身。
-
命令读取 @FocusedValue(\.deleteAction) var deleteAction。
用户点击菜单中的“删除”时,系统会从 焦点 元素开始遍历视图层级,查找最近定义的 deleteAction。这种“最内层优先”的逻辑完美复现了 AppKit 响应者链,同时具备类型安全性,且采用声明式写法。^34^
5.2 上下文菜单与菜单
contextMenu 修饰符的映射如下:
-
iOS: 触感触控(长按)。
-
macOS: 右键单击。
-
visionOS: 长捏合(或专用按钮)。
visionOS 警告: Apple 指南不建议在 visionOS 上过度使用上下文菜单,因为“长捏合”手势比右键单击或轻点更费力,需要持续保持肌肉紧张。因此,在 iOS 上隐藏在上下文菜单中的操作,在 visionOS 上通常应放到顶层 UI 中,例如放在装饰控件或专用的“更多”(...)按钮中。^19^
与其为列表中的每一行都附加 .contextMenu,不如优先使用现代的 .contextMenu(forSelectionType:) 修饰符。这个较新的修饰符针对列表的 选中内容 操作,性能更好,也更符合 macOS 和 iPadOS 的多选功能。^36^
5.3 装饰控件:空间工具栏
visionOS 用 装饰控件 取代了工具栏。SwiftUI 会自动将 .toolbar 内容映射到底部装饰控件,而开发者也可以使用 .ornament(attachmentAnchor:) 修饰符显式创建“侧边装饰控件”。
从架构上看,装饰控件“附着”在窗口上,但位于独立的 Z 平面。它们是工具面板(例如画笔、颜色)的理想位置:既能常驻,又不会喧宾夺主。与固定不变的 macOS 工具栏不同,装饰控件营造出“浮动面板”的感觉,用户能直观地理解可以抓取它们(不过目前它们会随窗口一起移动)。^37^
6. visionOS 特性:共享空间的坐标
为 visionOS 开发,需要从二维坐标转变为三维变换的思维方式。即使在“共享空间”(应用以窗口形式呈现)中,坐标系也很重要。
6.1 体积窗口与 Z 深度
带有 .windowStyle(.volumetric) 的 WindowGroup 会创建一个 体积窗口。这是一个有界的三维盒体。
-
裁剪: 体积窗口中的内容会被裁剪到其边界范围内。三维角色无法走出体积窗口的范围。
-
交互: 用户可以旋转整个体积窗口。这意味着应用必须处理方向变化,但变化的不是设备方向,而是 容器 相对于用户的方向。
体积窗口中一个常见的问题是 缩放比例。如果开发者导入通用的 USDZ 模型,它在真实世界单位中可能高达 100 米。在体积窗口中,这样的模型要么会小到几乎看不见,要么会被严格裁剪。开发者必须调整所有三维资源的缩放比例,使其适配标准体积窗口尺寸(通常为 1-2 米)。^7^
6.2 应用的“正面”
在 iOS 中,“正面”指屏幕。在 visionOS 中,“正面”是相对于应用启动时用户头部的位置而言的。
系统提供了重置功能(长按 Digital Crown),可重新居中所有应用。应用必须监听此操作,并确保其内容重新对齐。
不过,开发者无法以编程方式强制窗口移动到用户房间(共享空间)中的特定坐标。这是一项隐私限制,用于防止应用在墙上叠加垃圾信息或追踪用户的移动。窗口的变换由用户控制;应用只能控制窗口内的内容。1
7. 将无障碍与包容性融入架构
在多平台设计中,无障碍功能往往被视为合规要求;但在 SwiftUI 和 visionOS 的语境下,它是架构的核心支柱。
7.1 目标尺寸与物理特性
-
iOS: 最小目标尺寸为 44x44 点。
-
visionOS: 最小目标尺寸为 60x60 点(虚拟尺寸)。
为什么会有差异?眼动追踪存在自然的“抖动”(扫视)。小于 60 pt 的目标很难可靠地注视。如果应用将密集的 iOS 工具栏移植到 visionOS,却不调整间距,用户就会不断误触。
解决方案:使用自适应内边距或 ViewThatFits,增大 visionOS 布局中的间距。38
7.2 无障碍操作
.accessibilityAction 修饰符允许开发者将复杂手势映射为简单的语义操作。
-
场景: “摇一摇撤销”手势,或 iPad 上的“三指轻扫”。
-
问题: 肢体动作能力受限的用户无法执行这些操作。visionOS 用户也可能觉得这些操作很累。
-
解决方案: .accessibilityAction(named: "Undo") { undo() }。
这会将操作提供给语音控制、切换控制和辅助功能菜单,确保无论使用何种输入方式,都能使用应用的功能。在 visionOS 上,这一点至关重要,因为有些用户可能无法完美执行“捏合”手势,因而需要依赖停留控制或语音。^38^
8. 实现策略:代码结构
要实现这一统一架构,代码库应采用特定的结构模式。
8.1 “核心逻辑-UI”分离
-
核心逻辑(模型): 纯 Swift。尽可能不导入 SwiftUI。负责处理数据、持久化和业务逻辑。
-
状态层(视图模型): ObservableObject 或 @Observable。负责将数据映射到 UI 状态(例如,“侧边栏是否已打开?”、“当前选中了什么?”)。
-
界面层(视图): 高度模块化的 SwiftUI 视图。
-
原子: 按钮、标签(使用 .hoverEffect、.contentShape 设置样式)。
-
分子: 列表行、卡片(使用 ViewThatFits 进行布局)。
-
有机体: NavigationSplitView / TabView(使用 sidebarAdaptable)。
-
8.2 多平台测试
测试不能只依赖模拟器。
-
触控模拟: 用于 iPad 的 Mac 模拟器在布局方面十分准确,但无法模拟“手指太粗”问题。
-
注视模拟: visionOS 模拟器允许使用鼠标模拟眼睛。这适用于功能测试,但不适合可用性测试。没有实际头显,就无法评估眼睛疲劳程度或目标获取难度。
-
指针模拟: 在 iPad 上,必须连接触控板来测试“悬停”效果。许多悬停状态在 Mac 上看起来正常,但由于磁吸效果,在 iPad 上会感觉“发黏”或不对劲。^39^
9. 结论
iOS、iPadOS、macOS 和 visionOS 的融合,并非 硬件 的融合——这些设备在物理形态上仍然各不相同——而是 架构 的融合。visionOS 引入的“空间”设计语言已延伸至平面平台,为整个生态系统带来了深度、分层和感知意图的交互。
要构建连贯的多平台应用,开发者需要摒弃“以设备为中心”的思维方式(例如,“如果是 iPhone,就执行 X”),转而采用“以情境为中心”的思维方式(例如,“如果是紧凑宽度,就执行 X;如果连接了指点设备,就执行 Y”)。借助 Scene 层级结构、sidebarAdaptable 布局引擎、FocusedValue 命令链和统一的 SpatialTapGesture 输入模型,开发者可以构建出无论在手中、桌面上还是房间里使用,都能让人感觉原生的应用。Apple 开发的未来,不只是编写能在任何地方运行的代码,更是编写在任何地方都恰如其分的代码。
表 2:统一输入映射策略
语义 iOS iPadOS macOS visionOS 操作 (触控) (触控/指针) (指针/键盘) (注视/手部)
主要选择 轻点 轻点/单击 单击 注视 + 捏合
次要 长按 长按/右键 右键 长时间捏合/ 操作 (触觉反馈) “更多”按钮
滚动 滑动 滑动/双指平移 滚轮/触控板 捏合 + 拖动 手部
缩放 双指张开 双指张开 捏合(触控板) 双手 张开
焦点/目标 不适用(直接) 悬停(磁吸) 悬停(光标) 注视(悬停 效果)
文本输入 虚拟键盘 虚拟键盘/实体键盘 实体键盘 虚拟键盘/ 听写
表 3:生命周期状态的影响
ScenePhase 系统行为 开发者 (一般情况) 职责
Active 应用获得完整的 CPU/GPU 启动动画、 优先级。 连接套接字、启用 传感器。
Inactive 应用可见但未处于焦点 暂停高负载渲染 状态(例如 Mac 的 (游戏),保存草稿 后台窗口)。 状态。
Background 应用不可见。可能很快 **关键:**释放 被挂起。 高负载资源、关闭 文件句柄、停止 位置更新。在 visionOS 上,暂停所有 ARKit 会话。
表 4:命令基础设施实现
组件 作用 实现
菜单栏 全局命令发现 .commands { CommandMenu(...) } on WindowGroup.
FocusedValue “响应者链”链接 @FocusedValue(\.key) var action in Commands; .focusedValue(\.key, value) in Views.
KeyboardShortcut 高级用户激活 .keyboardShortcut("s", modifiers:.command) on Buttons.
AccessibilityAction 辅助技术触发 .accessibilityAction(named: "Save") { save() }.
引用资料
-
设计 visionOS | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos]{.underline}
-
App 图标 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/design/human-interface-guidelines/app-icons]{.underline}
-
人机界面指南 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/design/human-interface-guidelines]{.underline}
-
呈现窗口和空间 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/documentation/visionos/presenting-windows-and-spaces]{.underline}
-
支持多个窗口 --- App 开发教程 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/tutorials/app-dev-training/supporting-multiple-windows]{.underline}
-
场景。你是否曾想过 SwiftUI…… | Michael-Andre Odusami 著 - Medium,访问日期:2025 年 11 月 24 日, [https://medium.com/@michaelodusami/swiftui-scenes-29778723d48b]{.underline}
-
如何在沉浸式空间中添加窗口。- visionosdev - Reddit,访问日期:2025 年 11 月 24 日, [https://www.reddit.com/r/visionosdev/comments/1ctn03e/how_to_add_windows_in_an_immersive_space/]{.underline}
-
使用 SwiftUI 构建和自定义菜单栏 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/Building-and-customizing-the-menu-bar-with-SwiftUI]{.underline}
-
使用标签页导航增强 App 内容 | Apple 开发者文档,访问日期:2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/Enhancing-your-app-content-with-tab-navigation]{.underline}
-
ImmersiveSpace | Apple Developer Documentation,访问于 2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/ImmersiveSpace]{.underline}
-
探索 visionOS 中的沉浸式空间 - Create with Swift,访问于 2025 年 11 月 24 日, [https://www.createwithswift.com/exploring-immersive-spaces-in-visionos/]{.underline}
-
如何从一个沉浸式空间切换到另一个——第二部分 - Step Into Vision,访问于 2025 年 11 月 24 日, [https://stepinto.vision/example-code/how-to-transition-from-one-immersive-space-to-another-part-two/]{.underline}
-
ScenePhase | Apple Developer Documentation,访问于 2025 年 11 月 24 日, [https://developer.apple.com/documentation/swiftui/scenephase]{.underline}
-
使用场景阶段实现交互:在 SwiftUI 中运用 @Environment(.scenePhase) | Wesley Matlock 著 | Medium,访问于 2025 年 11 月 24 日, [https://medium.com/@wesleymatlock/interactivity-with-scene-phases-harnessing-environment-scenephase-in-swiftui-935da9c36874]{.underline}
-
探索 Navigation Split View - Create with Swift,访问于 2025 年 11 月 24 日, [https://www.createwithswift.com/exploring-the-navigationsplitview/]{.underline}
-
Axel Le Pennec:“采用新的 TabView 设计……” - iOS Dev Space,访问于 2025 年 11 月 24 日, [https://iosdev.space/@alpennec/113465183815948633]{.underline}
-
sidebarAdaptable | Apple Developer Documentation,访问于 2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable]{.underline}
-
使用 iOS 18 的新版 TabView 和侧边栏 - Donny Wals,访问于 2025 年 11 月 24 日, [https://www.donnywals.com/using-ios-18s-new-tabview-with-a-sidebar/]{.underline}
-
在 iOS 18 上使用 SwiftUI 改进后的带侧边栏 TabView - iOS Coffee Break,访问于 2025 年 11 月 24 日, [https://www.ioscoffeebreak.com/issue/issue34]{.underline}
-
从顶部栏到侧边栏:sidebarAdaptable TabView 样式 - YouTube,访问于 2025 年 11 月 24 日, [https://www.youtube.com/watch?v=YcrMLBmz0dA]{.underline}
-
根据窗口/屏幕宽度同时采用 NavigationSplitView 和 TabView,访问于 2025 年 11 月 24 日, [https://stackoverflow.com/questions/79665096/adopting-both-navigationsplitview-and-tabview-depending-on-window-screen-width]{.underline}
-
SwiftUI Navigation Stack 的理想 TabView 行为 | Akshay Mahajan 著, 访问于 2025 年 11 月 24 日, [https://betterprogramming.pub/swiftui-navigation-stack-and-ideal-tab-view-behaviour-e514cc41a029]{.underline}
-
精通 SwiftUI iOS 16 中的 ViewThatFits | Husnain Ali 著 | Medium,访问于 2025 年 11 月 24 日, [https://medium.com/@husnainali593/mastering-viewthatfits-in-swiftui-ios-16-9af7e642a84a]{.underline}
-
SwiftUI 布局的奇妙案例:为什么那个 Spacer 的作用与你想的不一样 | Wesley Matlock | Medium,访问于 2025 年 11 月 24 日, [https://medium.com/@wesleymatlock/the-curious-case-of-swiftui-layout-why-that-spacer-isnt-doing-what-you-think-86d7cede277c]{.underline}
-
深入探索 visionOS 中的操控,访问于 2025 年 11 月 24 日, [https://stepinto.vision/articles/deep-dive-into-manipulation-on-visionos/]{.underline}
-
SpatialTapGesture | Apple 开发者文档,访问于 2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/SpatialTapGesture]{.underline}
-
在 visionOS 的沉浸式空间中检测手势(SwiftUI)- Stack Overflow, 访问于 2025 年 11 月 24 日, [https://stackoverflow.com/questions/77712163/detect-gesture-in-immersive-space-visionos-swiftui]{.underline}
-
空间 SwiftUI:hoverEffect 修饰符 - Step Into Vision,访问于 2025 年 11 月 24 日, [https://stepinto.vision/example-code/spatial-swiftui-hovereffect-modifier/]{.underline}
-
CustomHoverEffect | Apple 开发者文档,访问于 2025 年 11 月 24 日, [https://developer.apple.com/documentation/SwiftUI/CustomHoverEffect]{.underline}
-
眼睛 | Apple 开发者文档,访问于 2025 年 11 月 24 日, [https://developer.apple.com/design/human-interface-guidelines/eyes]{.underline}
-
掌握 SwiftUI 中的 FocusState 属性包装器 - Swift with Majid, 访问于 2025 年 11 月 24 日, [https://swiftwithmajid.com/2021/08/24/mastering-focusstate-property-wrapper-in-swiftui/]{.underline}
-
如何检测同时按下附件视图并触发空间轻点手势 - Reddit,访问于 2025 年 11 月 24 日, [https://www.reddit.com/r/visionosdev/comments/1ezumz7/how_to_detect_pushing_both_of_attachment_view_and/]{.underline}
-
使用 SwiftUI 识别多点触控手势 - Stack Overflow, 访问于 2025 年 11 月 24 日, [https://stackoverflow.com/questions/74475743/recognizing-a-multitouch-gesture-with-swiftui]{.underline}
-
SwiftUI 中的 FocusedValues - Shadowfacts,访问于 2025 年 11 月 24 日, [https://shadowfacts.net/2025/focused-values/]{.underline}
-
在 visionOS 中创建高级悬停效果 - Create with Swift, 访问于 2025 年 11 月 24 日, [https://www.createwithswift.com/creating-advanced-hover-effects-in-visionos/]{.underline}
-
我在 SwiftUI Menu 中发现的一个小修饰符。用户轻点菜单而不是长按时,可以让菜单像按钮一样操作。长按则会显示菜单操作。从 iOS 15 起就有了 - Reddit,访问于 2025 年 11 月 24 日, [https://www.reddit.com/r/SwiftUI/comments/1i5hxlm/small_modifier_i_found_with_the_swiftui_menu_when/]{.underline}
-
visionOS 上 WindowGroup 与 ImmersiveSpace Entity 之间的连接,访问于 2025 年 11 月 24 日, [https://stackoverflow.com/questions/78169409/connection-between-windowgroup-and-immersivespace-entity-on-visionos]{.underline}
-
如何使用 SwiftUI 解决 iOS 移动应用中的常见无障碍挑战,访问于 2025 年 11 月 24 日, [https://www.freecodecamp.org/news/how-to-address-ios-accessibility-challenges-using-swiftui/]{.underline}
-
启用跨平台触控交互:指针事件与触摸事件 - Christian Liebel,访问于 2025 年 11 月 24 日, [https://christianliebel.com/2015/05/enabling-cross-platform-touch-interactions-pointer-vs-touch-events/]{.underline}