SwiftUI 窗口、Scene 与 多窗口架构
Source: Dev.to
App vs Scene(最重要的区别)
App
定义你的应用是什么,拥有全局配置,并创建 scene。
Scene
定义你的应用如何呈现,拥有窗口生命周期,并且可以同时存在多个实例。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
规则: 你的应用可以拥有多个 scene,每个 scene 可以拥有多个窗口。
WindowGroup 实际做了什么
WindowGroup {
ContentView()
}
- iOS: 多个应用窗口(iPad)
- macOS: 多个窗口
- visionOS: 多个空间实例
每个窗口:
- 拥有自己的视图层级
- 拥有自己的状态
- 不会 自动共享
ViewModel
最大的多窗口 Bug
@StateObject var vm = GlobalViewModel()
在 WindowGroup 内部放置 @StateObject 是错误的,因为:
- 每个窗口都会得到一个新实例
- 状态在窗口之间分叉
- 导航会不同步
正确的全局状态放置位置
全局状态必须位于 scene 之上:
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(appState)
}
}
}
现在:
- 所有窗口共享同一状态
- 导航保持一致
- 数据保持同步
Scene‑Local 状态 vs. App‑Global 状态
Scene‑local(场景本地):
- 导航栈
- 选中项
- 焦点
- 滚动位置
App‑global(应用全局):
- 认证
- 用户会话
- 缓存
- 功能标记
- 深度链接
切勿混用它们。
ScenePhase 是每个 Scene 的(而非全局的)
@Environment(\.scenePhase) var scenePhase
每个窗口都有自己的阶段,这意味着:
- 将一个窗口置于后台 ≠ 整个应用进入后台
inactive≠ 被销毁active≠ 所有窗口都在前台
请明智地使用这些信息。
支持多种 Scene 类型
SwiftUI 允许你声明多种 scene 角色:
var body: some Scene {
WindowGroup("Main") {
MainView()
}
WindowGroup("Inspector") {
InspectorView()
}
Settings {
SettingsView()
}
}
典型用例:
- 检查器面板
- 设置窗口
- 辅助工具
- 调试覆盖层
编程方式打开新窗口
@Environment(\.openWindow) var openWindow
Button("Open Details") {
openWindow(id: "details")
}
定义要打开的窗口:
WindowGroup(id: "details") {
DetailView()
}
这种模式是桌面级 SwiftUI 应用管理额外窗口的方式。
窗口范围的依赖注入
每个窗口应接收:
- 自己的导航状态
- 共享的服务
- 共享的应用状态
示例:
WindowGroup {
RootView(
router: Router(),
services: services
)
}
services在所有窗口之间共享router(或类似的导航对象)是每个窗口独有的
测试多窗口行为
在测试时,验证:
- 可以正确打开和关闭多个窗口
- 将一个 scene 置于后台不会影响其他 scene
- 状态恢复在每个窗口上都能工作
- 共享状态的变更能够如预期传播
大多数 SwiftUI bug 只会在两个或更多窗口同时激活时显现。
最后思考
SwiftUI scene 是一种架构概念,而不是样板代码。理解以下要点:
- App 与 scene 的区别
- 窗口身份
- Scene‑local 与 global 状态的划分
- 多窗口行为
能够帮助你构建:
- 原生感
- 正确性
- 可扩展性的应用。