Mac 上的 SwiftUI:构建基础
SwiftUI on the Mac: Build the fundamentals
2021年6月10日
一句话判断
SwiftUI 终于在 macOS 上拿到了 Table、Commands 和 Toolbar 这三个做 Mac app 的必备组件——如果你之前因为「SwiftUI 做不出正经 Mac app」而犹豫,现在可以重新评估了。
这场 Session 讲了什么
SwiftUI 在 macOS 12 的改进让它终于可以用来构建生产级的 Mac app。Session 用一个完整的「菜谱管理」app 作为示例,展示了如何用纯 SwiftUI 构建 macOS app 的核心组件。
Table 是最大的新增组件。它对应 AppKit 的 NSTableView,支持多列、排序、选择(单选/多选)、自定义单元格。配合 TableColumn 和 TableRow,你可以用声明式代码创建复杂的数据表格。Table 在 iPadOS 上会自动降级为 List。
Commands 修饰符让你可以在 SwiftUI 中配置 Mac 的菜单栏。CommandGroup、CommandMenu 和 CommandButton 可以精确控制菜单的层级和快捷键。系统菜单(File、Edit、View)中的项也可以被替换或追加。
Toolbar 在 macOS 上的行为得到了完善。ToolbarItem 的 placement 参数现在可以精确控制项目在工具栏中的位置(principal、navigation、confirmationAction 等)。
值得深挖的点
Table 的数据绑定模式
Table 支持两种数据绑定:静态列(编译时确定列数)和动态列(运行时确定列数)。静态列使用 TableColumn 泛型,性能更好。动态列使用 columns 参数,更灵活。Session 建议:如果你的表格结构固定,用静态列;如果用户可以自定义显示哪些列,用动态列。
多窗口和状态管理
macOS app 通常需要支持多窗口。SwiftUI 的 WindowGroup 会自动处理多窗口的创建和管理。每个窗口有独立的 @State 和 @SceneStorage。如果需要跨窗口共享状态,用 @AppStorage 或者 @EnvironmentObject。
代码片段
// 使用 Table 构建数据表格
import SwiftUI
struct RecipeListView: View {
let recipes: [Recipe]
@State private var selection: Recipe.ID?
@State private var sortOrder = [KeyPathComparator(\Recipe.name)]
@State private var sortedRecipes: [Recipe] = []
var body: some View {
Table(sortedRecipes, selection: $selection, sortOrder: $sortOrder) {
TableColumn("名称", value: \.name) { recipe in
HStack {
Image(systemName: "fork.knife")
Text(recipe.name)
}
}
TableColumn("分类", value: \.category) { recipe in
Text(recipe.category.rawValue)
}
TableColumn("时间", value: \.cookingTime) { recipe in
Text("\(recipe.cookingTime) 分钟")
}
TableColumn("难度", value: \.difficulty) { recipe in
DifficultyIndicator(level: recipe.difficulty)
}
}
.onChange(of: sortOrder) { order in
sortedRecipes = recipes.sorted(using: order)
}
.onAppear {
sortedRecipes = recipes
}
}
}
struct Recipe: Identifiable {
let id: UUID
let name: String
let category: Category
let cookingTime: Int
let difficulty: Int
}
// 配置 Mac 菜单栏
@main
struct RecipeApp: App {
var body: some Scene {
WindowGroup {
RecipeListView(recipes: sampleRecipes)
}
.commands {
// 在文件菜单中添加项
CommandGroup(after: .newItem) {
Button("导入菜谱") {
importRecipe()
}
.keyboardShortcut("i", modifiers: [.command, .shift])
Button("导出菜谱") {
exportRecipes()
}
}
// 自定义菜单
CommandMenu("筛选") {
Button("显示所有") { filterRecipes(.all) }
.keyboardShortcut("0", modifiers: .command)
Button("仅显示甜点") { filterRecipes(.dessert) }
.keyboardShortcut("1", modifiers: .command)
Button("仅显示主食") { filterRecipes(.main) }
.keyboardShortcut("2", modifiers: .command)
}
}
}
}
// 配置 macOS 工具栏
struct RecipeDetailView: View {
let recipe: Recipe
var body: some View {
VStack {
Text(recipe.name)
.font(.title)
}
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button(action: editRecipe) {
Label("编辑", systemImage: "pencil")
}
Button(action: shareRecipe) {
Label("分享", systemImage: "square.and.arrow.up")
}
Button(action: printRecipe) {
Label("打印", systemImage: "printer")
}
}
ToolbarItem(placement: .navigation) {
Button(action: goBack) {
Label("返回", systemImage: "chevron.left")
}
}
}
}
}
最佳实践
在 macOS 上开发 SwiftUI app 时,用 NavigationSplitView 替代旧的 NavigationView。NavigationSplitView 会自动在 macOS 上创建侧边栏+内容的分栏布局,在 iPadOS 上也能正确适配。
菜单栏中的快捷键不要和系统快捷键冲突。避免使用 Cmd+Q、Cmd+H、Cmd+Space 等系统保留的快捷键。如果你的 app 有编辑功能,确保 Cmd+Z(撤销)和 Cmd+Shift+Z(重做)都有实现。
还有什么值得关注
Table在 iOS 上不支持,但会自动降级为List。用#if os(macOS)条件编译来区分平台。Settingsscene 可以创建标准的 Mac 偏好设置窗口。DocumentGroup可以创建标准的 Mac 文档编辑器,自动处理文件的新建、打开、保存。