SwiftUI 在 Mac 上的收尾工作
SwiftUI on the Mac: The finishing touches
2021年6月9日
一句话判断
如果你的 SwiftUI Mac App 看起来「有点不对劲」,这个 Session 就是你的修复手册——从窗口行为到菜单栏,把那些让原生 App 感觉「原生」的细节一次性讲清楚。
这场 Session 讲了什么
SwiftUI 在 macOS 上的基础功能在 WWDC 2020 已经有了,这个 Session 聚焦在「收尾」——让你的 Mac App 不只是能跑,而是真正像一个 Mac App。Session 分几个主题展开:窗口管理(多窗口、窗口尺寸约束)、菜单栏集成(菜单命令、右键菜单)、工具栏设计(toolbar 的正确用法)、以及键盘快捷键。
窗口管理方面,macOS 12 新增了 defaultSize 和 windowResizability 修饰符,让你可以精确控制窗口的初始大小和缩放范围。以前的 SwiftUI Mac App 窗口大小经常「不对」——要么太大要么太小,用户得手动调整。现在你可以设置理想的默认尺寸和最小/最大尺寸。
菜单栏方面,SwiftUI 的 CommandGroup 和 CommandMenu API 终于可以覆盖大部分 Mac App 的菜单需求。Session 展示了如何把「文件 > 新建窗口」、编辑操作、以及自定义的 App 专属命令正确地集成到菜单栏中。关键是你需要理解 SwiftUI 的命令系统是声明式的——你定义的是「这个命令存在」,而不是「点这个菜单项时执行这段代码」。
值得深挖的点
windowResizability(.contentSize) 的陷阱。 当你使用 .contentSize 修饰符时,窗口的尺寸会根据内容自动调整。但如果你有一个可滚动的列表或动态内容,窗口大小可能会不可预测地跳动。Session 的建议是:对于固定布局的窗口(比如设置面板、关于对话框),使用 .contentSize;对于有可变内容的窗口(比如文档编辑器),使用 .contentMinSize 并手动设置最小尺寸。.windowResizability(.userAllowed) 是最灵活的选项,让用户自由调整但受你的 min/max 约束。
focusedValue 和 FocusSystem 的配合。 Mac App 的一个核心体验是「当前焦点」。当你选中一段文字时,编辑菜单的命令应该作用于这段文字;当你选中一个列表项时,命令应该作用于这个列表项。SwiftUI 的 focusedValue 修饰符让你可以把当前焦点关联的值「发布」出去,然后菜单命令通过 @FocusedValue 读取这些值。这解决了 SwiftUI Mac App 长期以来「菜单命令不知道该作用于什么」的问题。
代码片段
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// 设置窗口默认大小和缩放约束
.defaultSize(width: 800, height: 600)
.windowResizability(.contentSize)
// 设置面板——固定大小,不可缩放
Window("设置", id: "settings") {
SettingsView()
}
.windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar)
}
}
// 集成菜单栏命令
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
// 替换「新建」命令
CommandGroup(replacing: .newItem) {
Button("新建窗口") {
openNewWindow()
}
.keyboardShortcut("n", modifiers: .command)
}
// 添加自定义菜单
CommandMenu("工具") {
Button("导出 PDF") {
exportPDF()
}
.keyboardShortcut("e", modifiers: [.command, .shift])
Divider()
Button("清除缓存") {
clearCache()
}
}
}
}
}
// 使用 focusedValue 让菜单命令感知当前焦点
struct DocumentView: View {
@State private var selectedText = ""
var body: some View {
TextEditor(text: $selectedText)
// 将选中的文字发布给焦点系统
.focusedValue(\.selectedText, selectedText)
}
}
// 在菜单命令中读取焦点值
struct DocumentCommands: Commands {
@FocusedValue(\.selectedText) var selectedText
var body: some Commands {
CommandGroup(after: .editMenu) {
Button("复制选中文字") {
copyToClipboard(selectedText ?? "")
}
.disabled(selectedText == nil || selectedText!.isEmpty)
}
}
}
最佳实践
- Mac App 的窗口默认尺寸应该基于内容而非屏幕尺寸——在 13 寸 MacBook 和 27 寸 iMac 上,同样的 800x600 窗口体感完全不同
- 菜单命令中必须正确处理
.disabled状态——在 Mac 上,用户会通过菜单灰掉的程度来判断当前能做什么 - 为你的 App 添加
Settingsscene(Settings而不是Window),这样系统会自动把它放在「App 名称 > 设置」菜单下 - 测试时在 System Preferences > Appearance 中切换 Light/Dark 模式和 Accent Color,确保你的 SwiftUI 视图正确响应
还有什么值得关注
- macOS 12 的
WindowGroup支持通过id参数创建多种类型的窗口 - SwiftUI 的
ToolbarItem在 macOS 上会自动适配为原生工具栏控件 openWindow和openDocument环境方法让你可以在代码中编程式地打开新窗口