Highlight
优秀的 Mac App 有四个共同特征:灵活(可定制)、熟悉(遵循系统约定)、广阔(利用大屏幕)、精确(高信息密度)。Session 用一个园艺管理 App 展示了如何用 SwiftUI 实现这些原则。
核心内容
SwiftUI 在 macOS Monterey 中的改进让它真正可以用来构建生产级 Mac App。Session 用一个园艺追踪 App 作为贯穿示例,从零开始构建。
四个核心原则的 SwiftUI 实现:
- 灵活:用
@SceneStorage保存侧边栏展开状态和选中项,让用户的界面配置在重启后保持 - 熟悉:用
Commands修饰符构建标准菜单栏,ToolbarItem放在约定位置 - 广阔:用
Table展示多列数据,DisclosureGroup控制内容显示 - 精确:Table 的紧凑行高、Sidebar 的最小宽度设置
详细内容
Sidebar:大纲视图的构建
Mac App 的 Sidebar 通常使用 DisclosureGroup 实现大纲视图。配合 @SceneStorage 可以保存展开状态(06:09)。
@SceneStorage("expansionState") private var expansionState = Set<String>()
var body: some View {
List {
DisclosureGroup("Current Year", isExpanded: $isCurrentExpanded) {
ForEach(currentYearGardens) { garden in
Label(garden.name, systemImage: "leaf")
.tag(garden.id)
}
}
DisclosureGroup("History") {
GardenHistoryOutline()
}
}
.frame(minWidth: 150)
}
关键点:
@SceneStorage自动在 App 退出时保存状态,重启后恢复DisclosureGroup提供可折叠的大纲视图tag()配合selectionbinding 支持键盘导航和选中状态
Table:多列数据展示
Table 是 macOS Monterey 新增的组件,类似 NSTableView(09:14)。
Table(plants, selection: $selectedPlant, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Variety", value: \.variety)
TableColumn("Days to Maturity") { plant in
Text("\(plant.daysToMaturity)")
}
}
关键点:
Table支持多列显示,比List适合更复杂的数据selectionbinding 支持单选或多选sortOrderbinding 自动支持列排序- 在 iPadOS 上自动降级为
List
Toolbar:工具栏的配置
工具栏是 Mac 用户发现功能的标准位置(11:21)。
Table(plants) {
// 列定义...
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: addPlant) {
Label("Add Plant", systemImage: "plus")
}
}
ToolbarItem(placement: .automatic) {
DisplayModePicker()
}
}
关键点:
ToolbarItem的placement决定在工具栏中的位置.primaryAction放在左侧显眼位置.automatic让系统根据空间决定位置- 可以根据选中状态禁用按钮
Searchable:搜索功能
Mac 上始终有硬件键盘,搜索是快速筛选数据的有效方式(12:15)。
Table(plants.filter { searchText.isEmpty || $0.name.contains(searchText) }) {
// 列定义...
}
.searchable(text: $searchText)
关键点:
.searchable修饰符添加搜索框到工具栏- Mac 上搜索框固定在工具栏右侧
- 支持
Cmd+F快捷键聚焦搜索框 - 自动处理焦点和键盘导航
Commands:菜单栏配置
菜单栏是 Mac 用户探索功能的重要途径(13:17)。
@main
struct GardenApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
SidebarCommands()
CommandGroup(before: .newItem) {
Button("Add Plant") {
// 添加植物
}
.keyboardShortcut("n", modifiers: .command)
}
CommandMenu("Plants") {
Button("Mark as Watered") {
// 标记为已浇水
}
.keyboardShortcut("w", modifiers: [.command, .shift])
}
}
}
}
关键点:
.commands修饰符配置菜单栏SidebarCommands()添加标准的侧边栏切换菜单CommandGroup(before:)在系统菜单前插入自定义命令CommandMenu创建新的顶级菜单
FocusedSceneValue:窗口间通信
多窗口 App 的菜单命令需要作用于前台窗口(14:01)。
struct GardenDetail: View {
@Binding var garden: Garden?
var body: some View {
Table(plants, selection: $selectedPlants) {
// 列定义...
}
.focusedSceneValue(\.selectedGarden, $garden)
.focusedSceneValue(\.selectedPlants, $selectedPlants)
}
}
struct PlantCommands: Commands {
@FocusedValue(\.selectedGarden) private var focusedGarden: Binding<Garden>?
var body: some Commands {
CommandMenu("Plants") {
Button("Mark as Watered") {
focusedGarden?.wrappedValue.markAsWatered()
}
.disabled(focusedGarden == nil)
}
}
}
关键点:
@FocusedValue读取当前焦点窗口的值focusedSceneValue暴露 binding 给菜单命令- 菜单命令自动作用于前台窗口
- 不需要手动追踪窗口焦点
核心启发
-
用 @SceneStorage 保存用户界面状态:Mac App 有多个窗口,每个窗口的状态应该独立保存。
@SceneStorage自动处理这一点,无需手动读写 UserDefaults。 -
用 Table 替代 List 展示复杂数据:需要多列、可排序的数据时,用
Table而不是List。Table专为信息密度设计,支持排序和选择,在 iPadOS 上自动降级为List。 -
为所有操作提供菜单栏快捷键:Mac 用户期望通过菜单栏和快捷键访问所有功能。即使 App 有工具栏,也应该在菜单栏中重复这些操作,增加功能的可发现性。
-
用 Searchable 添加搜索功能:Mac 的工具栏空间充足,
.searchable让搜索框出现在标准位置,支持Cmd+F聚焦,符合用户习惯。 -
利用 FocusedSceneValue 实现窗口间协作:多窗口 App 的菜单命令需要作用于前台窗口。
focusedSceneValue+@FocusedValue实现了这一点,无需手动管理焦点追踪。
关联 Session
- SwiftUI on the Mac: Add the finishing touches — Mac App 的收尾工作和高级特性,包括拖拽、照片附件
- Build a great Mac app with SwiftUI — 用 SwiftUI 构建原生 Mac App 的完整指南
- What’s new in SwiftUI — SwiftUI 在 WWDC21 中的全面更新
- What’s new in AppKit — macOS 12 中 AppKit 的更新
评论
GitHub Issues · utterances