macos-design-guidelines by ehmo/platform-design-skills
npx skills add https://github.com/ehmo/platform-design-skills --skill macos-design-guidelinesMac 应用服务于专业用户,他们期望获得深度键盘控制、持久菜单栏、可调整大小的多窗口布局以及紧密的系统集成。本指南将苹果的 HIG 转化为可操作的规则,并附有 SwiftUI 和 AppKit 示例。
每个 Mac 应用都必须有菜单栏。它是命令的主要发现机制。找不到某个功能的用户会首先在菜单栏中寻找。
每个应用至少必须包含:应用、文件、编辑、视图、窗口、帮助。仅当应用不是基于文档时,才可以省略"文件"菜单。在"编辑"和"视图"之间,或"视图"和"窗口"之间添加应用特定的菜单。
// SwiftUI — 标准菜单结构
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
// 添加到现有的标准菜单
CommandGroup(after: .newItem) {
Button("从模板新建...") { newFromTemplate() }
.keyboardShortcut("T", modifiers: [.command, .shift])
}
CommandMenu("画布") {
Button("缩放至合适大小") { zoomToFit() }
.keyboardShortcut("0", modifiers: .command)
Divider()
Button("添加画板") { addArtboard() }
.keyboardShortcut("A", modifiers: [.command, .shift])
}
}
}
}
// AppKit — 以编程方式构建菜单
let editMenu = NSMenu(title: "编辑")
let undoItem = NSMenuItem(title: "撤销", action: #selector(UndoManager.undo), keyEquivalent: "z")
let redoItem = NSMenuItem(title: "重做", action: #selector(UndoManager.redo), keyEquivalent: "Z")
editMenu.addItem(undoItem)
editMenu.addItem(redoItem)
editMenu.addItem(.separator())
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
每个执行操作的菜单项都必须有键盘快捷键。标准操作使用标准快捷键(Cmd+C、Cmd+V、Cmd+Z 等)。自定义快捷键应使用 Cmd 加一个字母。将 Cmd+Shift、Cmd+Option 和 Cmd+Ctrl 组合键保留给次要操作。
标准快捷键参考:
| 操作 | 快捷键 |
|---|---|
| 新建 | Cmd+N |
| 打开 | Cmd+O |
| 关闭 | Cmd+W |
| 保存 | Cmd+S |
| 另存为 | Cmd+Shift+S |
| 打印 | Cmd+P |
| 撤销 | Cmd+Z |
| 重做 | Cmd+Shift+Z |
| 剪切 | Cmd+X |
| 复制 | Cmd+C |
| 粘贴 | Cmd+V |
| 全选 | Cmd+A |
| 查找 | Cmd+F |
| 查找下一个 | Cmd+G |
| 偏好设置/设置 | Cmd+, |
| 隐藏应用 | Cmd+H |
| 退出 | Cmd+Q |
| 最小化 | Cmd+M |
| 全屏 | Cmd+Ctrl+F |
菜单项必须反映当前状态。禁用不适用的项。更新标题以匹配上下文(例如,"撤销键入"而不仅仅是"撤销")。对于开/关状态使用切换复选标记。
// SwiftUI — 在现有工具栏菜单命令旁添加侧边栏切换
CommandGroup(after: .toolbar) {
Button(showingSidebar ? "隐藏侧边栏" : "显示侧边栏") {
showingSidebar.toggle()
}
.keyboardShortcut("S", modifiers: [.command, .control])
}
// AppKit — 验证菜单项
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(delete(_:)) {
menuItem.title = selectedItems.count > 1 ? "删除 \(selectedItems.count) 个项目" : "删除"
return !selectedItems.isEmpty
}
return super.validateMenuItem(menuItem)
}
在所有交互元素上提供右键单击上下文菜单。上下文菜单应包含菜单栏中与被点击元素最相关的操作子集,以及特定于该元素的操作。
// SwiftUI
Text(item.name)
.contextMenu {
Button("重命名...") { rename(item) }
Button("复制") { duplicate(item) }
Divider()
Button("删除", role: .destructive) { delete(item) }
}
应用菜单(最左侧,粗体应用名称)必须包含:关于、偏好设置/设置(Cmd+,)、服务子菜单、隐藏应用(Cmd+H)、隐藏其他(Cmd+Option+H)、显示全部、退出(Cmd+Q)。切勿重命名或删除这些标准项。
// SwiftUI — 设置场景
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
Settings { SettingsView() } // 自动连接到 Cmd+,
}
}
将菜单栏视为应用的命令记忆。将常用操作放在一致的菜单中,使用稳定的名称和快捷键,以便用户快速识别,而不是寻找特定于上下文的变体。
Mac 用户期望完全控制窗口的大小、位置和生命周期。一个与窗口管理作斗争的应用在 Mac 上会感觉从根本上就是坏的。
所有主窗口必须可以自由调整大小。设置一个保持界面可用的最小尺寸。除非内容确实无法缩放(罕见),否则切勿设置最大尺寸。
// SwiftUI
WindowGroup {
ContentView()
.frame(minWidth: 600, minHeight: 400)
}
.defaultSize(width: 900, height: 600)
// AppKit
window.minSize = NSSize(width: 600, height: 400)
window.setContentSize(NSSize(width: 900, height: 600))
通过设置适当的窗口集合行为来启用原生全屏。绿色交通灯按钮必须要么进入全屏,要么显示分屏选择器。
// AppKit
window.collectionBehavior.insert(.fullScreenPrimary)
SwiftUI 窗口会自动获得全屏支持。
除非你的应用是单一用途的工具,否则应支持多窗口。基于文档的应用必须允许同时打开多个文档。在 SwiftUI 中使用 WindowGroup 或 DocumentGroup。
// SwiftUI — 基于文档的应用
@main
struct TextEditorApp: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
TextEditorView(document: file.$document)
}
}
}
对于基于文档的应用,标题栏必须显示文档名称。支持代理图标拖拽。显示编辑状态(关闭按钮中的圆点)。支持点击标题栏重命名。
// AppKit
window.representedURL = document.fileURL
window.title = document.displayName
window.isDocumentEdited = document.hasUnsavedChanges
// SwiftUI — NavigationSplitView 标题
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
.navigationTitle(document.name)
}
在多次启动之间持久化窗口位置、大小和状态。使用 NSWindow.setFrameAutosaveName 或 SwiftUI 的内置状态恢复功能。
// AppKit
window.setFrameAutosaveName("MainWindow")
// SwiftUI — WindowGroup 自动处理
WindowGroup(id: "main") {
ContentView()
}
.defaultPosition(.center)
切勿隐藏或重新定位关闭(红色)、最小化(黄色)或缩放(绿色)按钮。它们必须保持在左上角。如果使用自定义标题栏,按钮仍然必须可见且功能正常。
// AppKit — 保留交通灯的自定义标题栏
window.titlebarAppearsTransparent = true
window.styleMask.insert(.fullSizeContentView)
// 交通灯保持功能正常且可见
工具栏是菜单栏之后的次要命令界面。它们提供对频繁操作的快速访问,并且应该是可定制的。
使用统一的标题栏 + 工具栏样式以获得现代外观。工具栏位于标题栏区域,节省垂直空间。
// SwiftUI
WindowGroup {
ContentView()
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: compose) {
Label("撰写", systemImage: "square.and.pencil")
}
}
}
}
.windowToolbarStyle(.unified)
// AppKit
window.titleVisibility = .hidden
window.toolbarStyle = .unified
允许用户添加、移除和重新排列工具栏项目。提供一组默认项目和一组可用的超集项目。
// SwiftUI — 可定制的工具栏
.toolbar(id: "main") {
ToolbarItem(id: "compose", placement: .primaryAction) {
Button(action: compose) {
Label("撰写", systemImage: "square.and.pencil")
}
}
ToolbarItem(id: "filter", placement: .secondaryAction) {
Button(action: toggleFilter) {
Label("筛选", systemImage: "line.3.horizontal.decrease")
}
}
}
.toolbarRole(.editor)
在工具栏中使用分段控件或选择器来切换内容视图(例如,列表/网格/列)。这是一种工具栏模式,而不是标签栏。
// SwiftUI
ToolbarItem(placement: .principal) {
Picker("视图模式", selection: $viewMode) {
Label("列表", systemImage: "list.bullet").tag(ViewMode.list)
Label("网格", systemImage: "square.grid.2x2").tag(ViewMode.grid)
Label("列", systemImage: "rectangle.split.3x1").tag(ViewMode.column)
}
.pickerStyle(.segmented)
}
将搜索字段放在工具栏的尾部区域。在 SwiftUI 中使用 .searchable() 以获得带有建议和标记的标准搜索行为。
// SwiftUI
NavigationSplitView {
SidebarView()
} detail: {
ContentListView()
.searchable(text: $searchText, placement: .toolbar, prompt: "搜索项目")
.searchSuggestions {
ForEach(suggestions) { suggestion in
Text(suggestion.title).searchCompletion(suggestion.title)
}
}
}
工具栏项目应同时具有图标(SF Symbol)和文本标签。在紧凑模式下,仅显示图标。为了可发现性,优先使用带标签的图标。使用 Label 来提供两者。
侧边栏是 Mac 应用的主要导航界面。它们出现在前导边缘,并提供对顶级部分和内容库的持久访问。
将侧边栏放在左侧(前导)边缘。通过工具栏按钮或键盘快捷键使其可折叠。苹果没有定义通用的侧边栏快捷键——为你的应用选择一个合适的(例如,Cmd+Ctrl+S 很常见,但不能保证在所有应用中都是空闲的)。持久化折叠状态。
// SwiftUI
NavigationSplitView(columnVisibility: $columnVisibility) {
List(selection: $selection) {
Section("库") {
Label("所有项目", systemImage: "tray.full")
Label("收藏夹", systemImage: "star")
Label("最近", systemImage: "clock")
}
Section("标签") {
ForEach(tags) { tag in
Label(tag.name, systemImage: "tag")
}
}
}
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 320)
} detail: {
DetailView(selection: selection)
}
.navigationSplitViewStyle(.prominentDetail)
对内容库导航使用源列表样式(.listStyle(.sidebar))。源列表具有半透明背景,通过活力效果显示其后的桌面或窗口。
// SwiftUI
List(selection: $selection) {
ForEach(sections) { section in
Section(section.name) {
ForEach(section.items) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: item.icon)
}
}
}
}
}
.listStyle(.sidebar)
当内容是分层的(例如,文件夹树、项目结构)时,使用展开组或大纲视图让用户展开和折叠层级。
// SwiftUI — 递归大纲
List(selection: $selection) {
OutlineGroup(rootNodes, children: \.children) { node in
Label(node.name, systemImage: node.icon)
}
}
可以重新排序的侧边栏项目(书签、收藏夹、自定义部分)必须支持拖拽重新排序。实现 onMove 或 NSOutlineView 拖拽委托。
// SwiftUI
ForEach(favorites) { item in
Label(item.name, systemImage: item.icon)
}
.onMove { source, destination in
favorites.move(fromOffsets: source, toOffset: destination)
}
在侧边栏项目上显示徽章计数,用于未读数量、待处理项目或通知。使用 .badge() 修饰符。
// SwiftUI
Label("收件箱", systemImage: "tray")
.badge(unreadCount)
Mac 用户比其他任何平台都更依赖键盘快捷键。一个没有全面键盘支持的应用就是一个有缺陷的 Mac 应用。
每个可以通过鼠标访问的操作都必须有一个键盘等效操作。主要操作使用 Cmd+字母。次要操作使用 Cmd+Shift 或 Cmd+Option。第三级操作使用 Cmd+Ctrl。
键盘快捷键约定:
| 修饰键模式 | 用途 |
|---|---|
| Cmd+字母 | 主要操作(新建、打开、保存等) |
| Cmd+Shift+字母 | 主要操作的变体(另存为、查找上一个) |
| Cmd+Option+字母 | 替代模式(粘贴并匹配样式) |
| Cmd+Ctrl+字母 | 窗口/视图控制(全屏、侧边栏) |
| Ctrl+字母 | Emacs 风格的文本导航(可接受) |
| Fn+键 | 系统功能(F11 显示桌面等) |
支持 Tab 键在控件之间移动。在列表、网格和表格内支持方向键。支持 Shift+Tab 进行反向导航。在 SwiftUI 中使用 focusable() 和 @FocusState。
// SwiftUI — 焦点管理
struct ContentView: View {
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("名称", text: $name)
.focused($focusedField, equals: .name)
TextField("电子邮件", text: $email)
.focused($focusedField, equals: .email)
}
.onSubmit { advanceFocus() }
}
}
Esc 键必须关闭弹出窗口、表单、对话框并取消正在进行的操作。在文本字段中,Esc 键恢复为之前的值。在模态对话框中,Esc 键等同于点击取消。
// SwiftUI — 支持 Esc 的表单(自动)
.sheet(isPresented: $showingSheet) {
SheetView() // Esc 自动关闭
}
// AppKit — 自定义响应器
override func cancelOperation(_ sender: Any?) {
dismiss(nil)
}
在对话框和表单中,Return/Enter 键激活默认按钮(视觉上以蓝色强调)。默认按钮始终是最安全的主要操作。
// SwiftUI
Button("保存") { save() }
.keyboardShortcut(.defaultAction) // Enter 键
Button("取消") { cancel() }
.keyboardShortcut(.cancelAction) // Esc 键
Delete 键(退格键)必须移除列表、表格和集合中的选定项目。Cmd+Delete 用于更具破坏性的移除(移到废纸篓)。始终支持 Cmd+Z 撤销删除。
当项目支持预览时,空格键应调用快速查看。在 AppKit 中使用 QLPreviewPanel API,或在 SwiftUI 中使用 .quickLookPreview()。
// SwiftUI
List(selection: $selection) {
ForEach(files) { file in
FileRow(file: file)
}
}
.quickLookPreview($quickLookItem, in: files)
在列表和网格中,上/下方向键移动选择。左/右键展开/折叠展开组或导航列。Cmd+Up 跳到开头,Cmd+Down 跳到末尾。
Mac 是一个指针驱动的平台。每个交互元素都必须响应悬停、点击、右键点击和拖拽。
所有交互元素都必须有可见的悬停状态。按钮高亮显示,行显示选择指示器,链接改变光标。在 SwiftUI 中使用 .onHover。
// SwiftUI — 悬停效果
struct HoverableRow: View {
@State private var isHovered = false
var body: some View {
HStack {
Text(item.name)
Spacer()
if isHovered {
Button("编辑") { edit() }
.buttonStyle(.borderless)
}
}
.padding(8)
.background(isHovered ? Color.primary.opacity(0.05) : .clear)
.cornerRadius(6)
.onHover { hovering in isHovered = hovering }
}
}
每个交互元素都必须响应右键点击并显示上下文菜单。上下文菜单应包含与被点击项目最相关的操作。
支持拖放以进行内容操作:重新排序项目、在容器之间移动、从 Finder 导入文件以及导出内容。
// SwiftUI — 拖放
ForEach(items) { item in
ItemView(item: item)
.draggable(item)
}
.dropDestination(for: Item.self) { items, location in
handleDrop(items, at: location)
return true
}
// 接受来自 Finder 的文件拖放
.dropDestination(for: URL.self) { urls, location in
importFiles(urls)
return true
}
支持触控板(平滑/惯性)和鼠标滚轮(离散)滚动。在内容边界处使用弹性/反弹滚动。在适当的地方支持水平滚动。
改变光标以指示功能:可点击元素用指针、文本用 I 型光标、绘图用十字准线、窗口/分隔条边缘用调整大小手柄、可拖拽内容用抓手。
// AppKit — 自定义光标
override func resetCursorRects() {
addCursorRect(bounds, cursor: .crosshair)
}
在列表、表格和网格中支持 Cmd+点击进行非连续选择和 Shift+点击进行范围选择。这是一种根深蒂固的 Mac 交互模式。
// SwiftUI — 支持多选的表格
Table(items, selection: $selectedItems) {
TableColumn("名称", value: \.name)
TableColumn("日期", value: \.dateFormatted)
TableColumn("大小", value: \.sizeFormatted)
}
Mac 用户非常注重保护自己的注意力。只有在真正必要时才进行打断。
仅对发生在应用外部或需要用户操作的事件发送通知。切勿对常规操作发送通知。通知必须是可操作的。
// UserNotifications
let content = UNMutableNotificationContent()
content.title = "下载完成"
content.body = "project-assets.zip 已就绪"
content.categoryIdentifier = "DOWNLOAD"
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
对于重复出现的警告,提供一个"不再显示此消息"复选框。尊重用户的选择并持久化。
// AppKit — 带有抑制功能的警告
let alert = NSAlert()
alert.messageText = "从库中移除?"
alert.informativeText = "文件将被移到废纸篓。"
alert.alertStyle = .warning
alert.addButton(withTitle: "移除")
alert.addButton(withTitle: "取消")
alert.showsSuppressionButton = true
alert.suppressionButton?.title = "不再询问"
let response = alert.runModal()
if alert.suppressionButton?.state == .on {
UserDefaults.standard.set(true, forKey: "suppressRemoveAlert")
}
切勿对成功操作显示警告。改用内联状态指示器、工具栏徽章或微妙的动画。将模态警告保留给破坏性或不可逆的操作。
在 Dock 图标上显示通知计数的徽章。当用户处理通知后立即清除它。
// AppKit
NSApp.dockTile.badgeLabel = unreadCount > 0 ? "\(unreadCount)" : nil
常规操作应通过内联状态、工具栏状态或微妙的动画来确认完成。仅当用户必须停下来、评估后果并做出选择时,才使用模态警告。
Mac 应用存在于一个丰富的生态系统中。深度集成使应用感觉原生。
提供高质量的 1024x1024 应用图标。支持 Dock 右键点击菜单以进行快速操作。在 Dock 菜单中显示最近文档。
// AppKit — Dock 菜单
override func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let menu = NSMenu()
menu.addItem(withTitle: "新建窗口", action: #selector(newWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: "新建文档", action: #selector(newDocument(_:)), keyEquivalent: "")
menu.addItem(.separator())
for doc in recentDocuments.prefix(5) {
menu.addItem(withTitle: doc.name, action: #selector(openRecent(_:)), keyEquivalent: "")
}
return menu
}
使用 CSSearchableItem 和 Core Spotlight 为 Spotlight 搜索索引应用内容。用户期望通过 Cmd+Space 找到应用内容。
import CoreSpotlight
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.title = document.title
attributeSet.contentDescription = document.summary
attributeSet.thumbnailData = document.thumbnail?.pngData()
let item = CSSearchableItem(uniqueIdentifier: document.id, domainIdentifier: "documents", attributeSet: attributeSet)
CSSearchableIndex.default().indexSearchableItems([item])
通过快速查看预览扩展为自定义文件类型提供快速查看预览。用户期望按空格键预览 Finder 中的任何文件。
实现共享菜单,以便用户可以从你的应用共享内容到信息、邮件、备忘录等。同时接受来自其他应用的共享内容。
// SwiftUI
ShareLink(item: document.url) {
Label("共享", systemImage: "square.and.arrow.up")
}
注册服务菜单以接收来自其他应用的文本、URL 或文件。这是一个独特的 Mac 集成点,专业用户依赖于此。
通过提供 App Intents 来支持快捷指令应用。对于高级自动化,通过 .sdef 脚本字典添加 AppleScript/JXA 脚本支持。
// 用于快捷指令的 App Intents
struct CreateDocumentIntent: AppIntent {
static var title: LocalizedStringResource = "创建文档"
static var description = IntentDescription("使用给定标题创建新文档。")
@Parameter(title: "标题")
var title: String
func perform() async throws -> some IntentResult {
let doc = DocumentManager.shared.create(title: title)
return .result(value: doc.title)
}
}
Mac 应用的外观和感觉应该像属于这个平台。使用系统提供的材质、字体和颜色。
使用 SF Pro(系统字体)的标准动态类型大小。代码使用 SF Mono。切勿硬编码字体大小;使用语义样式。
// SwiftUI — 语义字体样式
Text("标题").font(.title)
Text("标题行").font(.headline)
Text("正文").font(.body)
Text("说明文字").font(.caption)
Text("let x = 42").font(.system(.body, design: .monospaced))
对侧边栏和工具栏背景使用系统材质。活力效果让桌面或底层内容透显出来,将应用锚定在 Mac 视觉语言中。
// SwiftUI
List { ... }
.listStyle(.sidebar) // 自动活力效果
// 自定义活力效果
ZStack {
VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
Text("侧边栏内容")
}
// AppKit — 视觉效果视图
let visualEffect = NSVisualEffectView()
visualEffect.material = .sidebar
visualEffect.blendingMode = .behindWindow
visualEffect.state = .followsWindowActiveState
对选择、强调和交互元素使用系统强调色。切勿用固定的品牌颜色覆盖标准控件的强调色。仅在自定义视图上适当使用 .accentColor 或 .tint。
// SwiftUI — 自动遵循系统强调色
Button("操作") { doSomething() }
.buttonStyle(.borderedProminent) // 使用系统强调色
Toggle("启用功能", isOn: $isEnabled) // 切换色调遵循强调色
每个视图都必须同时支持浅色和深色外观。使用语义颜色(Color.primary、Color.secondary、.background)而不是硬编码的颜色。在两种模式下进行测试。
// SwiftUI — 语义颜色
Text("标题").foregroundStyle(.primary)
Text("副标题").foregroundStyle(.secondary)
RoundedRectangle(cornerRadius: 8)
.fill(Color(nsColor: .controlBackgroundColor))
// 资源目录:为两种外观定义颜色
// 切勿对界面表面使用 Color.white 或 Color.black
尊重"减少透明度"辅助功能设置。当透明度减少时,用纯色背景替换半透明材质。
// SwiftUI
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
var body: some View {
if reduceTransparency {
Color(nsColor: .windowBackgroundColor)
} else {
VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
}
}
使用 20pt 的标准边距,相关控件之间使用 8pt 间距,组之间使用 20pt 间距。将控件对齐到网格。使用 SwiftUI 的内置间距或 AppKit 的 Auto Layout 并配合系统间距约束。
弹出窗口呈现锚定到某个控件的上下文内容。它们在 Mac 应用中很常见,用于选项面板、颜色选择器和上下文设置。
弹出窗口附加到源视图,并通过点击外部或按 Esc 键关闭。将它们用于应用于特定元素的设置或选项。不要将弹出窗口用于主要工作流程或多步骤操作。
// SwiftUI
Button("格式...") { showingFormatPopover = true }
.popover(isPresented: $showingFormatPopover, arrowEdge: .bottom) {
FormatOptionsView()
.frame(width: 280)
.padding()
}
当用户按下 Esc 键时,弹出窗口必须关闭。对于 .popover,SwiftUI 会自动处理。AppKit 的 NSPopover 在 behavior 设置为 .transient 或 .semitransient 时也会在 Esc 键按下时关闭。
为弹出窗口的内容设置合理的宽度。不要让弹出窗口比必要的更宽。除非列表本身很长(例如,字体选择器),否则内容不应需要滚动。
Mac 应用必须支持旁白、完整键盘访问、切换控制以及相关的辅助技术。
每个按钮、控件和交互元素都必须有有意义的辅助功能标签。仅图标的工具栏项目和图像按钮必须提供标签。
正确:
Button(action: deleteSelected) {
Image(systemName: "trash")
}
.accessibilityLabel("删除选定项目")
错误:
Button(action: deleteSelected) {
Image(systemName: "trash")
}
// 旁白读出 "trash" — 没有上下文时含义模糊
每个可以通过鼠标访问的操作也必须可以通过键盘访问。Tab 键必须在所有控件之间移动焦点。方向键必须在列表、表格和网格内导航。没有键盘陷阱。
// SwiftUI — 确保所有自定义视图都是可聚焦的
MyCustomControl()
.focusable()
.onKeyPress(.return) { handleActivation(); return .handled }
当用户启用减少动画时,禁用或替换装饰性动画。
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
ContentView()
.animation(reduceMotion ? nil : .spring(), value: isExpanded)
}
当启用减少透明度时,用纯色背景替换半透明材质(参见规则 9.5)。
旁白必须以逻辑阅读顺序遍历元素(对于从左到右的语言,从左上到右下)。当视觉布局不同时,使用 .accessibilitySortPriority() 或 accessibilityElement(children:) 来纠正顺序。
当用户在系统设置中启用粗体文本时,自定义渲染的文本必须适应。SwiftUI 文本样式会自动处理。对于 AppKit,检查 NSWorkspace.shared.accessibilityDisplayShouldUseBoldText,或者在 SwiftUI 中使用 @Environment(\.legibilityWeight) 对自定义文本应用更粗的权重。
正确:
// SwiftUI — 环境自动为标准样式处理粗体文本
Text("章节标题")
.font(.headline)
// SwiftUI — 自定义渲染响应 legibilityWeight
@Environment(\.legibilityWeight) var legibilityWeight
var body: some View {
Text("自定义标签")
.fontWeight(legibilityWeight == .bold ? .bold : .regular)
}
错误:
// 硬编码的权重忽略了粗体文本偏好
Text("自定义标签")
.fontWeight(.regular) // 从不适应粗体文本设置
当用户在系统设置中启用增加对比度时,自定义颜色必须提供更高对比度的变体。在 AppKit 中使用 NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast,或者在 SwiftUI 中使用 @Environment(\.colorSchemeContrast) 来检测并应用适当的值。
正确:
// SwiftUI
@Environment(\.colorSchemeContrast) var contrast
var borderColor: Color {
contrast == .increased ? Color.primary : Color.secondary
}
// AppKit
let shouldIncrease = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
let borderColor: NSColor = shouldIncrease ? .labelColor : .separatorColor
错误:
// 静态颜色忽略了增加对比度设置
let borderColor = NSColor.separatorColor // 始终低对比度;忽略用户偏好
| 快捷键 | 操作 |
|---|---|
| Cmd+N | 新建窗口/文档 |
| Cmd+O | 打开 |
| Cmd+W | 关闭窗口/标签页 |
| Cmd+Q | 退出应用 |
| Cmd+, | 设置/偏好设置 |
| Cmd+Tab | 切换应用 |
| Cmd+` | 在应用内切换窗口 |
| Cmd+T | 新建标签页 |
| 快捷键 | 操作 |
|---|---|
| Cmd+Z | 撤销 |
| Cmd+Shift+Z | 重做 |
| Cmd+X / C / V | 剪切 / 复制 / 粘贴 |
| Cmd+A | 全选 |
| Cmd+D | 复制 |
| Cmd+F | 查找 |
| Cmd+G | 查找下一个 |
| Cmd+Shift+G | 查找上一个 |
| Cmd+E | 使用选定内容进行查找 |
| 快捷键 | 操作 |
|---|---|
| Cmd+Ctrl+F | 切换全屏 |
| Cmd+Ctrl+S | 切换侧边栏(应用定义;不是通用的 HIG 标准) |
| Cmd++ / Cmd+- | 放大/缩小 |
| Cmd+0 | 实际大小 |
发布 Mac 应用前,请验证:
Mac apps serve power users who expect deep keyboard control, persistent menu bars, resizable multi-window layouts, and tight system integration. These guidelines codify Apple's HIG into actionable rules with SwiftUI and AppKit examples.
Every Mac app must have a menu bar. It is the primary discovery mechanism for commands. Users who cannot find a feature will look in the menu bar before anywhere else.
Every app must include at minimum: App , File , Edit , View , Window , Help. Omit File only if the app is not document-based. Add app-specific menus between Edit and View or between View and Window.
// SwiftUI — Standard menu structure
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
// Adds to existing standard menus
CommandGroup(after: .newItem) {
Button("New from Template...") { newFromTemplate() }
.keyboardShortcut("T", modifiers: [.command, .shift])
}
CommandMenu("Canvas") {
Button("Zoom to Fit") { zoomToFit() }
.keyboardShortcut("0", modifiers: .command)
Divider()
Button("Add Artboard") { addArtboard() }
.keyboardShortcut("A", modifiers: [.command, .shift])
}
}
}
}
// AppKit — Building menus programmatically
let editMenu = NSMenu(title: "Edit")
let undoItem = NSMenuItem(title: "Undo", action: #selector(UndoManager.undo), keyEquivalent: "z")
let redoItem = NSMenuItem(title: "Redo", action: #selector(UndoManager.redo), keyEquivalent: "Z")
editMenu.addItem(undoItem)
editMenu.addItem(redoItem)
editMenu.addItem(.separator())
Every menu item that performs an action must have a keyboard shortcut. Use standard shortcuts for standard actions (Cmd+C, Cmd+V, Cmd+Z, etc.). Custom shortcuts should use Cmd plus a letter. Reserve Cmd+Shift, Cmd+Option, and Cmd+Ctrl combos for secondary actions.
Standard Shortcut Reference:
| Action | Shortcut |
|---|---|
| New | Cmd+N |
| Open | Cmd+O |
| Close | Cmd+W |
| Save | Cmd+S |
| Save As | Cmd+Shift+S |
| Cmd+P | |
| Undo | Cmd+Z |
| Redo | Cmd+Shift+Z |
| Cut | Cmd+X |
| Copy | Cmd+C |
| Paste | Cmd+V |
| Select All | Cmd+A |
| Find | Cmd+F |
| Find Next | Cmd+G |
| Preferences/Settings | Cmd+, |
Menu items must reflect current state. Disable items that are not applicable. Update titles to match context (e.g., "Undo Typing" not just "Undo"). Toggle checkmarks for on/off states.
// SwiftUI — Add sidebar toggle alongside existing toolbar menu commands
CommandGroup(after: .toolbar) {
Button(showingSidebar ? "Hide Sidebar" : "Show Sidebar") {
showingSidebar.toggle()
}
.keyboardShortcut("S", modifiers: [.command, .control])
}
// AppKit — Validate menu items
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(delete(_:)) {
menuItem.title = selectedItems.count > 1 ? "Delete \(selectedItems.count) Items" : "Delete"
return !selectedItems.isEmpty
}
return super.validateMenuItem(menuItem)
}
Provide right-click context menus on all interactive elements. Context menus should contain the most relevant subset of menu bar actions for the clicked element, plus element-specific actions.
// SwiftUI
Text(item.name)
.contextMenu {
Button("Rename...") { rename(item) }
Button("Duplicate") { duplicate(item) }
Divider()
Button("Delete", role: .destructive) { delete(item) }
}
The App menu (leftmost, bold app name) must contain: About, Preferences/Settings (Cmd+,), Services submenu, Hide App (Cmd+H), Hide Others (Cmd+Option+H), Show All, Quit (Cmd+Q). Never rename or remove these standard items.
// SwiftUI — Settings scene
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
Settings { SettingsView() } // Automatically wired to Cmd+,
}
}
Treat the menu bar as the app's command memory. Keep common actions in consistent menus with stable names and shortcuts so users recognize them quickly instead of searching for context-specific variants.
Mac users expect full control over window size, position, and lifecycle. An app that fights window management feels fundamentally broken on the Mac.
All main windows must be freely resizable. Set a minimum size that keeps the UI usable. Never set a maximum size unless the content truly cannot scale (rare).
// SwiftUI
WindowGroup {
ContentView()
.frame(minWidth: 600, minHeight: 400)
}
.defaultSize(width: 900, height: 600)
// AppKit
window.minSize = NSSize(width: 600, height: 400)
window.setContentSize(NSSize(width: 900, height: 600))
Opt into native fullscreen by setting the appropriate window collection behavior. The green traffic-light button must either enter fullscreen or show the tile picker.
// AppKit
window.collectionBehavior.insert(.fullScreenPrimary)
SwiftUI windows get fullscreen support automatically.
Unless your app is a single-purpose utility, support multiple windows. Document-based apps must allow multiple documents open simultaneously. Use WindowGroup or DocumentGroup in SwiftUI.
// SwiftUI — Document-based app
@main
struct TextEditorApp: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
TextEditorView(document: file.$document)
}
}
}
For document-based apps, the title bar must show the document name. Support proxy icon dragging. Show edited state (dot in close button). Support title bar renaming on click.
// AppKit
window.representedURL = document.fileURL
window.title = document.displayName
window.isDocumentEdited = document.hasUnsavedChanges
// SwiftUI — NavigationSplitView titles
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
.navigationTitle(document.name)
}
Persist window position, size, and state across launches. Use NSWindow.setFrameAutosaveName or SwiftUI's built-in state restoration.
// AppKit
window.setFrameAutosaveName("MainWindow")
// SwiftUI — Automatic with WindowGroup
WindowGroup(id: "main") {
ContentView()
}
.defaultPosition(.center)
Never hide or reposition the close (red), minimize (yellow), or zoom (green) buttons. They must remain in the top-left corner. If using a custom title bar, the buttons must still be visible and functional.
// AppKit — Custom title bar that preserves traffic lights
window.titlebarAppearsTransparent = true
window.styleMask.insert(.fullSizeContentView)
// Traffic lights remain functional and visible
Toolbars are the secondary command surface after the menu bar. They provide quick access to frequent actions and should be customizable.
Use the unified title bar + toolbar style for a modern appearance. The toolbar sits in the title bar area, saving vertical space.
// SwiftUI
WindowGroup {
ContentView()
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: compose) {
Label("Compose", systemImage: "square.and.pencil")
}
}
}
}
.windowToolbarStyle(.unified)
// AppKit
window.titleVisibility = .hidden
window.toolbarStyle = .unified
Allow users to add, remove, and rearrange toolbar items. Provide a default set and a superset of available items.
// SwiftUI — Customizable toolbar
.toolbar(id: "main") {
ToolbarItem(id: "compose", placement: .primaryAction) {
Button(action: compose) {
Label("Compose", systemImage: "square.and.pencil")
}
}
ToolbarItem(id: "filter", placement: .secondaryAction) {
Button(action: toggleFilter) {
Label("Filter", systemImage: "line.3.horizontal.decrease")
}
}
}
.toolbarRole(.editor)
Use a segmented control or picker in the toolbar for switching between content views (e.g., List/Grid/Column). This is a toolbar pattern, not a tab bar.
// SwiftUI
ToolbarItem(placement: .principal) {
Picker("View Mode", selection: $viewMode) {
Label("List", systemImage: "list.bullet").tag(ViewMode.list)
Label("Grid", systemImage: "square.grid.2x2").tag(ViewMode.grid)
Label("Column", systemImage: "rectangle.split.3x1").tag(ViewMode.column)
}
.pickerStyle(.segmented)
}
Place the search field in the trailing area of the toolbar. Use .searchable() in SwiftUI for standard search behavior with suggestions and tokens.
// SwiftUI
NavigationSplitView {
SidebarView()
} detail: {
ContentListView()
.searchable(text: $searchText, placement: .toolbar, prompt: "Search items")
.searchSuggestions {
ForEach(suggestions) { suggestion in
Text(suggestion.title).searchCompletion(suggestion.title)
}
}
}
Toolbar items should have both an icon (SF Symbol) and a text label. In compact mode, show icons only. Prefer labeled icons for discoverability. Use Label to supply both.
Sidebars are the primary navigation surface for Mac apps. They appear on the leading edge and provide persistent access to top-level sections and content libraries.
Place the sidebar on the left (leading) edge. Make it collapsible via the toolbar button or a keyboard shortcut. Apple does not define a universal sidebar shortcut — choose one appropriate for your app (e.g., Cmd+Ctrl+S is common but not guaranteed to be free in all apps). Persist collapsed state.
// SwiftUI
NavigationSplitView(columnVisibility: $columnVisibility) {
List(selection: $selection) {
Section("Library") {
Label("All Items", systemImage: "tray.full")
Label("Favorites", systemImage: "star")
Label("Recent", systemImage: "clock")
}
Section("Tags") {
ForEach(tags) { tag in
Label(tag.name, systemImage: "tag")
}
}
}
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 320)
} detail: {
DetailView(selection: selection)
}
.navigationSplitViewStyle(.prominentDetail)
Use the source list style (.listStyle(.sidebar)) for content-library navigation. Source lists have a translucent background that shows the desktop or window behind them with vibrancy effects.
// SwiftUI
List(selection: $selection) {
ForEach(sections) { section in
Section(section.name) {
ForEach(section.items) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: item.icon)
}
}
}
}
}
.listStyle(.sidebar)
When content is hierarchical (e.g., folder trees, project structures), use disclosure groups or outline views to let users expand and collapse levels.
// SwiftUI — Recursive outline
List(selection: $selection) {
OutlineGroup(rootNodes, children: \.children) { node in
Label(node.name, systemImage: node.icon)
}
}
Sidebar items that can be reordered (bookmarks, favorites, custom sections) must support drag-to-reorder. Implement onMove or NSOutlineView drag delegates.
// SwiftUI
ForEach(favorites) { item in
Label(item.name, systemImage: item.icon)
}
.onMove { source, destination in
favorites.move(fromOffsets: source, toOffset: destination)
}
Show badge counts on sidebar items for unread counts, pending items, or notifications. Use the .badge() modifier.
// SwiftUI
Label("Inbox", systemImage: "tray")
.badge(unreadCount)
Mac users rely on keyboard shortcuts more than any other platform. An app without comprehensive keyboard support is a broken Mac app.
Every action reachable by mouse must have a keyboard equivalent. Primary actions use Cmd+letter. Secondary actions use Cmd+Shift or Cmd+Option. Tertiary actions use Cmd+Ctrl.
Keyboard Shortcut Conventions:
| Modifier Pattern | Usage |
|---|---|
| Cmd+letter | Primary actions (New, Open, Save, etc.) |
| Cmd+Shift+letter | Variant of primary (Save As, Find Previous) |
| Cmd+Option+letter | Alternative mode (Paste and Match Style) |
| Cmd+Ctrl+letter | Window/view controls (Fullscreen, Sidebar) |
| Ctrl+letter | Emacs-style text navigation (acceptable) |
| Fn+key | System functions (F11 Show Desktop, etc.) |
Support Tab to move between controls. Support arrow keys within lists, grids, and tables. Support Shift+Tab for reverse navigation. Use focusable() and @FocusState in SwiftUI.
// SwiftUI — Focus management
struct ContentView: View {
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
}
.onSubmit { advanceFocus() }
}
}
Esc must dismiss popovers, sheets, dialogs, and cancel in-progress operations. In text fields, Esc reverts to the previous value. In modal dialogs, Esc is equivalent to clicking Cancel.
// SwiftUI — Sheet with Esc support (automatic)
.sheet(isPresented: $showingSheet) {
SheetView() // Esc dismisses automatically
}
// AppKit — Custom responder
override func cancelOperation(_ sender: Any?) {
dismiss(nil)
}
In dialogs and forms, Return/Enter activates the default button (visually emphasized in blue). The default button is always the safest primary action.
// SwiftUI
Button("Save") { save() }
.keyboardShortcut(.defaultAction) // Enter key
Button("Cancel") { cancel() }
.keyboardShortcut(.cancelAction) // Esc key
The Delete key (Backspace) must remove selected items in lists, tables, and collections. Cmd+Delete for more destructive removal (move to Trash). Always support Cmd+Z to undo deletion.
When items support previewing, Space bar should invoke Quick Look. Use the QLPreviewPanel API in AppKit or .quickLookPreview() in SwiftUI.
// SwiftUI
List(selection: $selection) {
ForEach(files) { file in
FileRow(file: file)
}
}
.quickLookPreview($quickLookItem, in: files)
In lists and grids, Up/Down arrow keys move selection. Left/Right collapse/expand disclosure groups or navigate columns. Cmd+Up goes to the beginning, Cmd+Down goes to the end.
Mac is a pointer-driven platform. Every interactive element must respond to hover, click, right-click, and drag.
All interactive elements must have a visible hover state. Buttons highlight, rows show a selection indicator, links change cursor. Use .onHover in SwiftUI.
// SwiftUI — Hover effect
struct HoverableRow: View {
@State private var isHovered = false
var body: some View {
HStack {
Text(item.name)
Spacer()
if isHovered {
Button("Edit") { edit() }
.buttonStyle(.borderless)
}
}
.padding(8)
.background(isHovered ? Color.primary.opacity(0.05) : .clear)
.cornerRadius(6)
.onHover { hovering in isHovered = hovering }
}
}
Every interactive element must respond to right-click with a contextual menu. The context menu should contain the most relevant actions for the clicked item.
Support drag and drop for content manipulation: reordering items, moving between containers, importing files from Finder, and exporting content.
// SwiftUI — Drag and drop
ForEach(items) { item in
ItemView(item: item)
.draggable(item)
}
.dropDestination(for: Item.self) { items, location in
handleDrop(items, at: location)
return true
}
// Accepting file drops from Finder
.dropDestination(for: URL.self) { urls, location in
importFiles(urls)
return true
}
Support both trackpad (smooth/inertial) and mouse wheel (discrete) scrolling. Use elastic/bounce scrolling at content boundaries. Support horizontal scrolling where appropriate.
Change the cursor to indicate affordances: pointer for clickable elements, I-beam for text, crosshair for drawing, resize handles at window/splitter edges, grab hand for draggable content.
// AppKit — Custom cursor
override func resetCursorRects() {
addCursorRect(bounds, cursor: .crosshair)
}
Support Cmd+Click for non-contiguous selection and Shift+Click for range selection in lists, tables, and grids. This is a deeply ingrained Mac interaction pattern.
// SwiftUI — Tables with multi-selection
Table(items, selection: $selectedItems) {
TableColumn("Name", value: \.name)
TableColumn("Date", value: \.dateFormatted)
TableColumn("Size", value: \.sizeFormatted)
}
Mac users are protective of their attention. Only interrupt when truly necessary.
Send notifications only for events that happen outside the app or require user action. Never notify for routine operations. Notifications must be actionable.
// UserNotifications
let content = UNMutableNotificationContent()
content.title = "Download Complete"
content.body = "project-assets.zip is ready"
content.categoryIdentifier = "DOWNLOAD"
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
For recurring alerts, provide a "Do not show this again" checkbox. Respect the user's choice and persist it.
// AppKit — Alert with suppression
let alert = NSAlert()
alert.messageText = "Remove from library?"
alert.informativeText = "The file will be moved to the Trash."
alert.alertStyle = .warning
alert.addButton(withTitle: "Remove")
alert.addButton(withTitle: "Cancel")
alert.showsSuppressionButton = true
alert.suppressionButton?.title = "Do not ask again"
let response = alert.runModal()
if alert.suppressionButton?.state == .on {
UserDefaults.standard.set(true, forKey: "suppressRemoveAlert")
}
Never show alerts for successful operations. Use inline status indicators, toolbar badges, or subtle animations instead. Reserve modal alerts for destructive or irreversible actions.
Show a badge on the Dock icon for notification counts. Clear it promptly when the user addresses the notifications.
// AppKit
NSApp.dockTile.badgeLabel = unreadCount > 0 ? "\(unreadCount)" : nil
Routine actions should acknowledge completion with inline status, toolbar state, or a subtle animation. Use modal alerts only when the user must stop, evaluate consequences, and choose.
Mac apps exist in a rich ecosystem. Deep integration makes an app feel native.
Provide a high-quality 1024x1024 app icon. Support Dock right-click menus for quick actions. Show recent documents in the Dock menu.
// AppKit — Dock menu
override func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let menu = NSMenu()
menu.addItem(withTitle: "New Window", action: #selector(newWindow(_:)), keyEquivalent: "")
menu.addItem(withTitle: "New Document", action: #selector(newDocument(_:)), keyEquivalent: "")
menu.addItem(.separator())
for doc in recentDocuments.prefix(5) {
menu.addItem(withTitle: doc.name, action: #selector(openRecent(_:)), keyEquivalent: "")
}
return menu
}
Index app content for Spotlight search using CSSearchableItem and Core Spotlight. Users expect to find app content via Cmd+Space.
import CoreSpotlight
let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
attributeSet.title = document.title
attributeSet.contentDescription = document.summary
attributeSet.thumbnailData = document.thumbnail?.pngData()
let item = CSSearchableItem(uniqueIdentifier: document.id, domainIdentifier: "documents", attributeSet: attributeSet)
CSSearchableIndex.default().indexSearchableItems([item])
Provide Quick Look previews for custom file types via a Quick Look Preview Extension. Users expect Space to preview any file in Finder.
Implement the Share menu so users can share content from your app to Messages, Mail, Notes, etc. Also accept shared content from other apps.
// SwiftUI
ShareLink(item: document.url) {
Label("Share", systemImage: "square.and.arrow.up")
}
Register for the Services menu to receive text, URLs, or files from other apps. This is a uniquely Mac integration point that power users rely on.
Support the Shortcuts app by providing App Intents. For advanced automation, add AppleScript/JXA scripting support via an .sdef scripting dictionary.
// App Intents for Shortcuts
struct CreateDocumentIntent: AppIntent {
static var title: LocalizedStringResource = "Create Document"
static var description = IntentDescription("Creates a new document with the given title.")
@Parameter(title: "Title")
var title: String
func perform() async throws -> some IntentResult {
let doc = DocumentManager.shared.create(title: title)
return .result(value: doc.title)
}
}
Mac apps should look and feel like they belong on the platform. Use system-provided materials, fonts, and colors.
Use SF Pro (the system font) at standard dynamic type sizes. Use SF Mono for code. Never hardcode font sizes; use semantic styles.
// SwiftUI — Semantic font styles
Text("Title").font(.title)
Text("Headline").font(.headline)
Text("Body text").font(.body)
Text("Caption").font(.caption)
Text("let x = 42").font(.system(.body, design: .monospaced))
Use system materials for sidebar and toolbar backgrounds. Vibrancy lets the desktop or underlying content show through, anchoring the app to the Mac visual language.
// SwiftUI
List { ... }
.listStyle(.sidebar) // Automatic vibrancy
// Custom vibrancy
ZStack {
VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
Text("Sidebar Content")
}
// AppKit — Visual effect view
let visualEffect = NSVisualEffectView()
visualEffect.material = .sidebar
visualEffect.blendingMode = .behindWindow
visualEffect.state = .followsWindowActiveState
Use the system accent color for selection, emphasis, and interactive elements. Never override it with a fixed brand color for standard controls. Use .accentColor or .tint only on custom views when appropriate.
// SwiftUI — Follows system accent automatically
Button("Action") { doSomething() }
.buttonStyle(.borderedProminent) // Uses system accent color
Toggle("Enable feature", isOn: $isEnabled) // Toggle tint follows accent
Every view must support both Light and Dark appearances. Use semantic colors (Color.primary, Color.secondary, .background) rather than hardcoded colors. Test in both modes.
// SwiftUI — Semantic colors
Text("Title").foregroundStyle(.primary)
Text("Subtitle").foregroundStyle(.secondary)
RoundedRectangle(cornerRadius: 8)
.fill(Color(nsColor: .controlBackgroundColor))
// Asset catalog: define colors for Both Appearances
// Never use Color.white or Color.black for UI surfaces
Respect the "Reduce transparency" accessibility setting. When transparency is reduced, replace translucent materials with solid backgrounds.
// SwiftUI
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
var body: some View {
if reduceTransparency {
Color(nsColor: .windowBackgroundColor)
} else {
VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
}
}
Use 20pt standard margins, 8pt spacing between related controls, 20pt spacing between groups. Align controls to a grid. Use SwiftUI's built-in spacing or AppKit's Auto Layout with system spacing constraints.
Popovers present contextual content anchored to a control. They are common in Mac apps for options panels, color pickers, and contextual settings.
Popovers attach to a source view and are dismissed by clicking outside or pressing Esc. Use them for settings or options that apply to a specific element. Do not use popovers for primary workflows or multi-step operations.
// SwiftUI
Button("Format...") { showingFormatPopover = true }
.popover(isPresented: $showingFormatPopover, arrowEdge: .bottom) {
FormatOptionsView()
.frame(width: 280)
.padding()
}
Popovers must close when the user presses Esc. SwiftUI handles this automatically for .popover. AppKit's NSPopover also dismisses on Esc when behavior is set to .transient or .semitransient.
Set a reasonable width for the popover's content. Do not let the popover be wider than necessary. Content should not require scrolling unless the list is inherently long (e.g., a font picker).
Mac apps must support VoiceOver, Full Keyboard Access, Switch Control, and related assistive technologies.
Every button, control, and interactive element must have a meaningful accessibility label. Icon-only toolbar items and image buttons must provide labels.
Correct:
Button(action: deleteSelected) {
Image(systemName: "trash")
}
.accessibilityLabel("Delete selected items")
Incorrect:
Button(action: deleteSelected) {
Image(systemName: "trash")
}
// VoiceOver reads "trash" — ambiguous without context
Every action reachable by mouse must also be reachable by keyboard. Tab must move focus between all controls. Arrow keys must navigate within lists, tables, and grids. No keyboard traps.
// SwiftUI — Ensure all custom views are focusable
MyCustomControl()
.focusable()
.onKeyPress(.return) { handleActivation(); return .handled }
Disable or substitute decorative animations when the user enables Reduce Motion.
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
ContentView()
.animation(reduceMotion ? nil : .spring(), value: isExpanded)
}
Replace translucent materials with solid backgrounds when Reduce Transparency is enabled (see Rule 9.5).
VoiceOver must traverse elements in a logical reading order (top-left to bottom-right for LTR). Use .accessibilitySortPriority() or accessibilityElement(children:) to correct order when the visual layout diverges.
When the user enables Bold Text in System Settings, custom-rendered text must adapt. SwiftUI text styles handle this automatically. For AppKit, check NSWorkspace.shared.accessibilityDisplayShouldUseBoldText, or use @Environment(\.legibilityWeight) in SwiftUI to apply heavier weights to custom text.
Correct:
// SwiftUI — environment handles bold text automatically for standard styles
Text("Section Header")
.font(.headline)
// SwiftUI — custom rendering responds to legibilityWeight
@Environment(\.legibilityWeight) var legibilityWeight
var body: some View {
Text("Custom Label")
.fontWeight(legibilityWeight == .bold ? .bold : .regular)
}
Incorrect:
// Hardcoded weight ignores Bold Text preference
Text("Custom Label")
.fontWeight(.regular) // Never adapts to Bold Text setting
When the user enables Increase Contrast in System Settings, custom colors must provide higher-contrast variants. Use NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast in AppKit, or @Environment(\.colorSchemeContrast) in SwiftUI to detect and apply appropriate values.
Correct:
// SwiftUI
@Environment(\.colorSchemeContrast) var contrast
var borderColor: Color {
contrast == .increased ? Color.primary : Color.secondary
}
// AppKit
let shouldIncrease = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
let borderColor: NSColor = shouldIncrease ? .labelColor : .separatorColor
Incorrect:
// Static color ignores Increase Contrast setting
let borderColor = NSColor.separatorColor // Always low-contrast; ignores user preference
| Shortcut | Action |
|---|---|
| Cmd+N | New window/document |
| Cmd+O | Open |
| Cmd+W | Close window/tab |
| Cmd+Q | Quit app |
| Cmd+, | Settings/Preferences |
| Cmd+Tab | Switch apps |
| Cmd+` | Switch windows within app |
| Cmd+T | New tab |
| Shortcut | Action |
|---|---|
| Cmd+Z | Undo |
| Cmd+Shift+Z | Redo |
| Cmd+X / C / V | Cut / Copy / Paste |
| Cmd+A | Select All |
| Cmd+D | Duplicate |
| Cmd+F | Find |
| Cmd+G | Find Next |
| Cmd+Shift+G | Find Previous |
| Cmd+E | Use Selection for Find |
| Shortcut | Action |
|---|---|
| Cmd+Ctrl+F | Toggle fullscreen |
| Cmd+Ctrl+S | Toggle sidebar (app-defined; not a universal HIG standard) |
| Cmd++ / Cmd+- | Zoom in/out |
| Cmd+0 | Actual size |
Before shipping a Mac app, verify:
accessibilityDisplayShouldUseBoldText)colorSchemeContrast or accessibilityDisplayShouldIncreaseContrast)Do not do these things in a Mac app:
No menu bar — Every Mac app needs a menu bar. Period. A Mac app without menus is like a car without a steering wheel.
Hamburger menus — Never use a hamburger menu on Mac. The menu bar exists for this purpose. Hamburger menus signal a lazy iOS port.
Tab bars at the bottom — Mac apps use sidebars and toolbars, not iOS-style tab bars. If you need tabs, use actual document tabs in the tab bar (like Safari or Finder).
Large touch-sized targets — Mac controls should be compact (22-28pt height). Users have precise pointer input. Giant buttons waste space and look out of place.
Floating action buttons — FABs are a Material Design pattern. On Mac, place primary actions in the toolbar, menu bar, or as inline buttons.
Sheet for every action — Don't use modal sheets for simple operations. Use popovers, inline editing, or direct manipulation. Sheets should be reserved for multi-step workflows or important decisions.
Custom window chrome — Don't replace the standard title bar, traffic lights, or window controls with custom implementations. Users expect these to work consistently across all apps.
Ignoring keyboard — If a power user must reach for the mouse to perform common actions, your keyboard support is insufficient.
Single-window only — Unless your app is genuinely single-purpose (calculator, timer), support multiple windows. Users expect to Cmd+N for new windows.
Fixed window size — Non-resizable windows feel broken on Mac. Users have displays ranging from 13" laptops to 32" externals and expect to use that space.
Weekly Installs
2.0K
Repository
GitHub Stars
283
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex1.7K
opencode1.7K
gemini-cli1.7K
github-copilot1.7K
kimi-cli1.6K
amp1.6K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
| Hide App |
| Cmd+H |
| Quit | Cmd+Q |
| Minimize | Cmd+M |
| Fullscreen | Cmd+Ctrl+F |
No Cmd+Z undo — Every destructive or modifying action must be undoable. Users build muscle memory around Cmd+Z as their safety net.
Notification spam — Mac apps that send excessive notifications get their permissions revoked. Only notify for events that genuinely need attention.
Ignoring Dark Mode — A Mac app that looks wrong in Dark Mode appears abandoned. Always test both appearances.
Hardcoded colors — Use semantic system colors, not hardcoded hex values. Your colors should adapt to Light/Dark mode and accessibility settings automatically.
No drag and drop — Mac is a drag-and-drop platform. If users can see content, they expect to drag it somewhere.