mcpField
M
macos-hig-designer

macos-hig-designer

github.com/designnotdrum设计与 UI1122 次安装39 周前更新
代码仓库

关于 macos-hig-designer

用于依据 Apple 人机界面指南设计和审查原生 macOS 应用的技能,涵盖 macOS Tahoe 的 Liquid Glass。提供 SwiftUI 模式、API 更新建议、无障碍实践和界面合规检查清单。

主要功能

  1. 01Liquid Glass 设计指引
  2. 02SwiftUI 导航与窗口模式
  3. 03工具栏、菜单和快捷键模式
  4. 04无障碍实现建议
  5. 05弃用 API 替换方案
  6. 06HIG 审查清单

适用场景

  1. 01检查 macOS 界面代码是否符合 HIG
  2. 02更新旧版 SwiftUI 与 AppKit 模式
  3. 03设计无障碍的应用导航和窗口

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/designnotdrum/skills --skill macos-hig-designer

安装后在 Claude Code 中输入/macos-hig-designer确认已加载。

详细介绍

macOS HIG 设计指南

遵循 Apple 人机界面指南,使用 macOS Tahoe 的 Liquid Glass 设计系统打造原生 macOS 应用。

工作流决策树

User Request
    │
    ├─► "Review my macOS UI code"
    │       └─► Run HIG Compliance Check (Section 11)
    │           └─► Report violations with fixes
    │
    ├─► "Modernize this macOS code"
    │       └─► Identify deprecated APIs
    │           └─► Apply Modern API Replacements (Section 10)
    │
    └─► "Build [feature] for macOS"
            └─► Design with HIG principles first
                └─► Implement with modern SwiftUI patterns

1. 设计原则(macOS Tahoe)

三大核心准则
原则描述实现方式
层级通过 Liquid Glass 的半透明效果呈现视觉层次使用 .glassEffect()、材质和景深
和谐硬件与软件之间的同心对齐使用圆角、统一的半径和流畅的形状
一致性根据情境调整的平台惯例遵循标准模式,尊重用户偏好
Liquid Glass 设计理念

Liquid Glass 将透明度、反射、折射和流动感与磨砂质感相结合:

// macOS Tahoe Liquid Glass effect
.glassEffect()                    // Primary Liquid Glass material
.glassEffect(.regular.tinted)     // Tinted variant (26.1+)

// Pre-Tahoe fallback
.background(.ultraThinMaterial)
.background(.regularMaterial)
.background(.thickMaterial)

适合使用 Liquid Glass 的场景:

  • 侧边栏、工具栏和导航界面
  • 浮动面板和弹出框
  • Dock 和小组件背景
  • 系统级界面元素

不适合使用的场景:

  • 主要内容区域(文稿、媒体)
  • 密集的数据展示(表格、包含大量项目的列表)
  • 可读性至关重要的文本密集型界面

2. 导航模式

NavigationSplitView(主要模式)

适用于文稿类和内容密集型应用的三栏布局:

struct ContentView: View {
    @State private var selection: Item.ID?
    @State private var columnVisibility: NavigationSplitViewVisibility = .all

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            // Sidebar (source list)
            List(items, selection: $selection) { item in
                NavigationLink(value: item) {
                    Label(item.title, systemImage: item.icon)
                }
            }
            .navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300)
        } content: {
            // Content column (optional middle)
            ContentListView(selection: selection)
        } detail: {
            // Detail view
            DetailView(item: selectedItem)
        }
    }
}
侧边栏模式
// Source list with sections
List(selection: $selection) {
    Section("Library") {
        ForEach(libraryItems) { item in
            Label(item.name, systemImage: item.icon)
                .tag(item)
        }
    }

    Section("Collections") {
        ForEach(collections) { collection in
            Label(collection.name, systemImage: "folder")
                .tag(collection)
                .badge(collection.count)
        }
    }
}
.listStyle(.sidebar)
检视器面板(尾随侧边栏)
struct DocumentView: View {
    @State private var showInspector = true

    var body: some View {
        MainContentView()
            .inspector(isPresented: $showInspector) {
                InspectorView()
                    .inspectorColumnWidth(min: 200, ideal: 250, max: 400)
            }
            .toolbar {
                ToolbarItem {
                    Button {
                        showInspector.toggle()
                    } label: {
                        Label("Inspector", systemImage: "sidebar.trailing")
                    }
                }
            }
    }
}

3. 窗口管理

窗口配置
@main
struct MyApp: App {
    var body: some Scene {
        // Main document window
        WindowGroup {
            ContentView()
        }
        .windowStyle(.automatic)
        .windowToolbarStyle(.unified)
        .defaultSize(width: 900, height: 600)
        .defaultPosition(.center)

        // Settings window
        Settings {
            SettingsView()
        }

        // Utility window
        Window("Inspector", id: "inspector") {
            InspectorWindow()
        }
        .windowStyle(.plain)
        .windowResizability(.contentSize)
        .defaultPosition(.topTrailing)

        // Menu bar extra
        MenuBarExtra("Status", systemImage: "circle.fill") {
            StatusMenu()
        }
        .menuBarExtraStyle(.window)
    }
}
窗口样式
样式适用场景
.automatic标准应用窗口
.hiddenTitleBar以内容为中心的窗口(媒体播放器)
.plain实用工具窗口、面板
.unified集成式工具栏外观
.unifiedCompact紧凑工具栏高度
窗口状态恢复
WindowGroup {
    ContentView()
}
.handlesExternalEvents(matching: Set(arrayLiteral: "main"))
.commands {
    CommandGroup(replacing: .newItem) {
        Button("New Document") {
            // Handle new document
        }
        .keyboardShortcut("n")
    }
}
基于文稿的应用
@main
struct DocumentApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: MyDocument()) { file in
            DocumentView(document: file.$document)
        }
        .commands {
            CommandGroup(after: .saveItem) {
                Button("Export...") { }
                    .keyboardShortcut("e", modifiers: [.command, .shift])
            }
        }
    }
}

struct MyDocument: FileDocument {
    static var readableContentTypes: [UTType] { [.plainText] }

    init(configuration: ReadConfiguration) throws { }
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { }
}

4. 工具栏和菜单栏

工具栏配置
.toolbar {
    // Leading items (macOS places these before title)
    ToolbarItem(placement: .navigation) {
        Button(action: goBack) {
            Label("Back", systemImage: "chevron.left")
        }
    }

    // Principal (centered)
    ToolbarItem(placement: .principal) {
        Picker("View Mode", selection: $viewMode) {
            Label("Icons", systemImage: "square.grid.2x2").tag(ViewMode.icons)
            Label("List", systemImage: "list.bullet").tag(ViewMode.list)
        }
        .pickerStyle(.segmented)
    }

    // Trailing items
    ToolbarItemGroup(placement: .primaryAction) {
        Button(action: share) {
            Label("Share", systemImage: "square.and.arrow.up")
        }

        Button(action: toggleInspector) {
            Label("Inspector", systemImage: "sidebar.trailing")
        }
    }
}
.toolbarRole(.editor) // or .browser, .automatic
自定义菜单栏
.commands {
    // Replace existing menu group
    CommandGroup(replacing: .newItem) {
        Button("New Project") { }
            .keyboardShortcut("n")
        Button("New from Template...") { }
            .keyboardShortcut("n", modifiers: [.command, .shift])
    }

    // Add to existing group
    CommandGroup(after: .sidebar) {
        Button("Toggle Inspector") { }
            .keyboardShortcut("i", modifiers: [.command, .option])
    }

    // Custom menu
    CommandMenu("Canvas") {
        Button("Zoom In") { }
            .keyboardShortcut("+")
        Button("Zoom Out") { }
            .keyboardShortcut("-")
        Divider()
        Button("Fit to Window") { }
            .keyboardShortcut("0")
    }
}
菜单栏应用
MenuBarExtra("App Status", systemImage: statusIcon) {
    VStack(alignment: .leading, spacing: 8) {
        Text("Status: \(status)")
            .font(.headline)

        Divider()

        Button("Open Main Window") {
            openWindow(id: "main")
        }

        Button("Quit") {
            NSApplication.shared.terminate(nil)
        }
        .keyboardShortcut("q")
    }
    .padding()
}
.menuBarExtraStyle(.window) // or .menu for simple dropdown

5. 键盘快捷键

macOS 标准快捷键

在适用时始终实现以下快捷键:

操作快捷键实现方式
新建⌘N.keyboardShortcut("n")
打开⌘O.keyboardShortcut("o")
保存⌘S.keyboardShortcut("s")
关闭⌘W.keyboardShortcut("w")
撤销⌘Z.keyboardShortcut("z")
重做⇧⌘Z.keyboardShortcut("z", modifiers: [.command, .shift])
剪切⌘X.keyboardShortcut("x")
拷贝⌘C.keyboardShortcut("c")
粘贴⌘V.keyboardShortcut("v")
全选⌘A.keyboardShortcut("a")
查找⌘F.keyboardShortcut("f")
设置⌘,.keyboardShortcut(",")
隐藏⌘H由系统处理
退出⌘Q由系统处理
自定义快捷键
Button("Toggle Sidebar") {
    toggleSidebar()
}
.keyboardShortcut("s", modifiers: [.command, .control])

// Function keys
Button("Refresh") { }
    .keyboardShortcut(KeyEquivalent.init(Character(UnicodeScalar(NSF5FunctionKey)!)))

// Arrow keys
Button("Next") { }
    .keyboardShortcut(.rightArrow)

6. 使用 Liquid Glass 的组件

控件尺寸
尺寸形状适用场景
极小圆角矩形紧凑工具栏、密集界面
小圆角矩形次要控件、侧边栏
标准圆角矩形主要控件(默认)
大胶囊形突出操作
特大胶囊形 + Glass主视觉号召性操作、引导流程
// Size modifiers
Button("Action") { }
    .controlSize(.mini)     // Smallest
    .controlSize(.small)    // Compact
    .controlSize(.regular)  // Default
    .controlSize(.large)    // Prominent
    .controlSize(.extraLarge) // Hero (macOS 15+)
按钮
// Primary action (prominent)
Button("Save Changes") { }
    .buttonStyle(.borderedProminent)
    .controlSize(.large)

// Secondary action
Button("Cancel") { }
    .buttonStyle(.bordered)

// Tertiary/link style
Button("Learn More") { }
    .buttonStyle(.plain)
    .foregroundStyle(.link)

// Destructive
Button("Delete", role: .destructive) { }
    .buttonStyle(.bordered)

// Toolbar button
Button { } label: {
    Label("Add", systemImage: "plus")
}
.buttonStyle(.borderless)
文本字段
// Standard text field
TextField("Search", text: $query)
    .textFieldStyle(.roundedBorder)

// Search field with tokens
TextField("Search", text: $query)
    .searchable(text: $query, tokens: $tokens) { token in
        Label(token.name, systemImage: token.icon)
    }

// Secure field
SecureField("Password", text: $password)
    .textFieldStyle(.roundedBorder)
表格
Table(items, selection: $selection) {
    TableColumn("Name", value: \.name)
        .width(min: 100, ideal: 150)

    TableColumn("Date") { item in
        Text(item.date, format: .dateTime)
    }
    .width(100)

    TableColumn("Status") { item in
        StatusBadge(status: item.status)
    }
    .width(80)
}
.tableStyle(.inset(alternatesRowBackgrounds: true))
.contextMenu(forSelectionType: Item.ID.self) { selection in
    Button("Open") { }
    Button("Delete", role: .destructive) { }
}
弹出框和工作表
// Popover
Button("Info") {
    showPopover = true
}
.popover(isPresented: $showPopover, arrowEdge: .bottom) {
    InfoView()
        .frame(width: 300, height: 200)
        .padding()
}

// Sheet
.sheet(isPresented: $showSheet) {
    SheetContent()
        .frame(minWidth: 400, minHeight: 300)
}

// Alert
.alert("Delete Item?", isPresented: $showAlert) {
    Button("Cancel", role: .cancel) { }
    Button("Delete", role: .destructive) {
        deleteItem()
    }
} message: {
    Text("This action cannot be undone.")
}

7. 字体排版和颜色

系统字体排版
// Semantic styles (preferred)
Text("Title").font(.largeTitle)      // 26pt bold
Text("Headline").font(.headline)      // 13pt semibold
Text("Subheadline").font(.subheadline) // 11pt regular
Text("Body").font(.body)              // 13pt regular
Text("Callout").font(.callout)        // 12pt regular
Text("Caption").font(.caption)        // 10pt regular
Text("Caption 2").font(.caption2)     // 10pt regular

// Monospaced for code
Text("let x = 1").font(.system(.body, design: .monospaced))
语义颜色
// Foreground
.foregroundStyle(.primary)            // Primary text
.foregroundStyle(.secondary)          // Secondary text
.foregroundStyle(.tertiary)           // Tertiary text
.foregroundStyle(.quaternary)         // Quaternary text

// Backgrounds
.background(.background)              // Window background
.background(.regularMaterial)         // Translucent material

// Accent colors
.tint(.accentColor)                   // App accent color
.foregroundStyle(.link)               // Clickable links

// Semantic colors
Color.red                             // System red (adapts to light/dark)
Color.blue                            // System blue
活力效果和材质
// Materials (adapt to background content)
.background(.ultraThinMaterial)       // Most transparent
.background(.thinMaterial)
.background(.regularMaterial)         // Default
.background(.thickMaterial)
.background(.ultraThickMaterial)      // Least transparent

// Vibrancy in sidebars
List { }
    .listStyle(.sidebar)
    .scrollContentBackground(.hidden)
    .background(.ultraThinMaterial)

8. 间距和布局

8 点网格系统
// Standard spacing values
VStack(spacing: 8) { }                // Standard
VStack(spacing: 16) { }               // Section spacing
VStack(spacing: 20) { }               // Group spacing

// Padding
.padding(8)                           // Tight
.padding(12)                          // Standard
.padding(16)                          // Comfortable
.padding(20)                          // Spacious

// Content margins
.contentMargins(16)                   // Uniform margins
.contentMargins(.horizontal, 20)      // Horizontal only
安全区域
// Respect toolbar safe area
.safeAreaInset(edge: .top) {
    ToolbarContent()
}

// Ignore safe area for backgrounds
.ignoresSafeArea(.container, edges: .top)

// Content that should avoid toolbar
.safeAreaPadding(.top)
最小触控/点击目标
// Minimum 44x44 points for clickable elements
Button { } label: {
    Image(systemName: "gear")
}
.frame(minWidth: 44, minHeight: 44)

// Use contentShape for larger hit areas
RoundedRectangle(cornerRadius: 8)
    .frame(width: 200, height: 100)
    .contentShape(Rectangle())
    .onTapGesture { }
自适应布局
// Responsive to window size
GeometryReader { geometry in
    if geometry.size.width > 600 {
        HStack { content }
    } else {
        VStack { content }
    }
}

// Grid that adapts
LazyVGrid(columns: [
    GridItem(.adaptive(minimum: 150, maximum: 250))
], spacing: 16) {
    ForEach(items) { ItemView(item: $0) }
}

9. 辅助功能

VoiceOver
// Labels and hints
Button { } label: {
    Image(systemName: "plus")
}
.accessibilityLabel("Add item")
.accessibilityHint("Creates a new item in your library")

// Grouping related elements
VStack {
    Text(item.title)
    Text(item.subtitle)
}
.accessibilityElement(children: .combine)

// Custom actions
.accessibilityAction(named: "Delete") {
    deleteItem()
}
键盘导航
// Focus management
@FocusState private var focusedField: Field?

TextField("Name", text: $name)
    .focused($focusedField, equals: .name)
    .onSubmit {
        focusedField = .email
    }

// Focusable custom views
.focusable()
.onMoveCommand { direction in
    handleArrowKey(direction)
}
动态字体
// Scales with user preference
Text("Content")
    .dynamicTypeSize(.large ... .accessibility3)

// Fixed size when necessary (use sparingly)
Text("Fixed")
    .dynamicTypeSize(.large)
减少动态效果
@Environment(\.accessibilityReduceMotion) var reduceMotion

.animation(reduceMotion ? .none : .spring(), value: isExpanded)

// Alternative non-animated transitions
.transaction { transaction in
    if reduceMotion {
        transaction.animation = nil
    }
}
高对比度
@Environment(\.colorSchemeContrast) var contrast

// Increase contrast when needed
.foregroundStyle(contrast == .increased ? .primary : .secondary)

10. 现代 API 替换方案

已弃用 → 现代替代方案
已弃用现代替代方案备注
NavigationViewNavigationSplitView / NavigationStackmacOS 使用分栏视图,简单流程使用堆栈
.navigationViewStyle(.columns)NavigationSplitView内置分栏支持
使用 NavigationLink 的 List { }.listStyle(.sidebar)NavigationSplitView 侧边栏正确的分栏视图行为
详情视图中的 .toolbar { ToolbarItem(...) }NavigationSplitView 上的 .toolbar工具栏应用于正确的作用域
NSWindowControllerWindowGroup / Window纯 SwiftUI 窗口管理
NSMenu / NSMenuItem.commands { } / CommandMenu声明式菜单
NSToolbar.toolbar { }SwiftUI 工具栏 API
NSTouchBar.touchBar { }SwiftUI Touch Bar
NSOpenPanel.begin().fileImporter()SwiftUI 文件对话框
NSSavePanel.begin().fileExporter()SwiftUI 保存对话框
用于材质的 .background(Color.clear).background(.regularMaterial)正确的材质支持
自定义模糊效果.glassEffect()(Tahoe)原生 Liquid Glass
AppKit 互操作(按需使用)
// Wrap AppKit view
struct NSViewWrapper: NSViewRepresentable {
    func makeNSView(context: Context) -> NSView {
        // Create and configure NSView
    }

    func updateNSView(_ nsView: NSView, context: Context) {
        // Update when SwiftUI state changes
    }
}

// Access NSWindow
.background(WindowAccessor { window in
    window?.titlebarAppearsTransparent = true
})

11. 审查清单

Liquid Glass 与材质
  • 侧边栏使用 .glassEffect() 或适当的材质
  • 工具栏具有半透明外观
  • 内容区域保持清晰(不过度透明)
  • 材质能正确适配浅色/深色模式
  • 为 Tahoe 之前的版本提供备用材质
导航与窗口
  • 多栏布局使用 NavigationSplitView
  • 侧边栏设有适当的最小/最大宽度限制
  • 提供检查器面板以显示详情/属性
  • 已配置窗口恢复
  • 基于文稿的应用使用 DocumentGroup
  • 已测试多种窗口尺寸
控件与交互
  • 控件尺寸适合使用场景
  • 主要操作使用 .borderedProminent
  • 使用 .destructive 角色正确标记破坏性操作
  • 表格具有上下文菜单
  • 弹出框尺寸合适
键盘与快捷键
  • 已实现标准快捷键(⌘N、⌘S、⌘W 等)
  • 自定义快捷键不会与系统快捷键冲突
  • 所有交互元素均可通过键盘访问
  • 焦点顺序符合逻辑
  • 菜单栏命令具有快捷键
辅助功能
  • 所有图像均有辅助功能标签
  • 自定义控件具有正确的角色
  • 已测试 VoiceOver
  • 键盘导航完整
  • 遵循“减少动态效果”设置
  • 已测试“高对比度”模式
平台约定
  • 应用使用系统外观(而非自定义窗口装饰)
  • 设置位于“偏好设置”窗口中(而非单独窗口)
  • 文件对话框使用系统工作表
  • 在适用场景支持拖放
  • 集成“服务”菜单(如适用)
视觉润色
  • 对齐 8 点网格
  • 全局间距一致
  • 使用语义颜色(可适配主题)
  • 字体排印遵循 SF Pro 指南
  • 触控目标最小尺寸为 44 pt

快速参考:按尺寸划分的控件形状

┌─────────────────────────────────────────────────────┐
│  Mini/Small/Medium      │    Large/XLarge           │
│  ┌──────────────────┐   │    ╭──────────────────╮   │
│  │   Rounded Rect   │   │    │     Capsule      │   │
│  └──────────────────┘   │    ╰──────────────────╯   │
│                         │                           │
│  Compact layouts        │    Hero actions           │
│  Toolbars, sidebars     │    Onboarding, CTAs       │
└─────────────────────────────────────────────────────┘

快速参考:窗口样式

┌─────────────────────────────────────────────────────┐
│  .automatic        Standard window with titlebar    │
│  .hiddenTitleBar   Full content, titlebar hidden    │
│  .plain            No chrome, utility panels        │
│  .unified          Toolbar merges with titlebar     │
│  .unifiedCompact   Compact unified toolbar          │
└─────────────────────────────────────────────────────┘

快速参考:导航模式

┌─────────────┬───────────────┬─────────────────────┐
│  Sidebar    │   Content     │      Detail         │
│             │               │                     │
│  Source     │   List or     │   Selected item     │
│  List       │   Grid        │   properties        │
│             │               │                     │
│  Collections│   Items       │   Inspector panel   │
│  Folders    │   Browse      │   Edit view         │
│             │               │                     │
│  Min: 180   │   Flexible    │   Min: 300          │
│  Max: 300   │               │   Ideal: 400+       │
└─────────────┴───────────────┴─────────────────────┘
       NavigationSplitView (three-column)

评论

支持 Markdown