重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
macos-hig-designer by designnotdrum/skills
npx skills add https://github.com/designnotdrum/skills --skill macos-hig-designer遵循 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
| 原则 | 描述 | 实现 |
|---|---|---|
| 层次结构 | 通过 Liquid Glass 半透明实现视觉层次 | 使用 .glassEffect()、材质和深度 |
| 和谐统一 | 硬件与软件之间的同心对齐 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 圆角、一致的半径、流畅的形状 |
| 一致性 | 适应上下文的平台约定 | 遵循标准模式,尊重用户偏好 |
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:
何时不使用:
适用于基于文档和内容密集型应用的三栏布局:
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")
}
}
}
}
}
@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 { }
}
.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
适用时始终实现以下快捷键:
| 操作 | 快捷键 | 实现 |
|---|---|---|
| 新建 | ⌘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)
| 尺寸 | 形状 | 使用场景 |
|---|---|---|
| Mini | 圆角矩形 | 紧凑工具栏、密集 UI |
| Small | 圆角矩形 | 次要控件、侧边栏 |
| Regular | 圆角矩形 | 主要控件(默认) |
| Large | 胶囊形 | 突出操作 |
| Extra Large | 胶囊形 + 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.")
}
// 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)
// 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) }
}
// 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)
| 已弃用 | 现代 | 备注 |
|---|---|---|
NavigationView | NavigationSplitView / NavigationStack | Split 用于 macOS,Stack 用于简单流程 |
.navigationViewStyle(.columns) | NavigationSplitView | 内置列支持 |
List { }.listStyle(.sidebar) with NavigationLink | NavigationSplitView sidebar | 正确的拆分视图行为 |
.toolbar { ToolbarItem(...) } in detail | .toolbar on NavigationSplitView | 工具栏应用于正确范围 |
NSWindowController | WindowGroup / 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) for materials | .background(.regularMaterial) | 正确的材质支持 |
| Custom blur effects | .glassEffect() (Tahoe) | 原生 Liquid Glass |
// 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
})
.glassEffect() 或适当的材质NavigationSplitViewDocumentGroup.borderedProminent.destructive 角色正确标记┌─────────────────────────────────────────────────────┐
│ 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)
每周安装次数
49
代码仓库
GitHub 星标数
1
首次出现
2026年2月1日
安全审计
安装于
opencode47
codex47
gemini-cli44
cursor44
claude-code42
github-copilot42
Design native macOS applications following Apple's Human Interface Guidelines with macOS Tahoe's Liquid Glass design system.
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
| Principle | Description | Implementation |
|---|---|---|
| Hierarchy | Visual layers through Liquid Glass translucency | Use .glassEffect(), materials, and depth |
| Harmony | Concentric alignment between hardware/software | Round corners, consistent radii, flowing shapes |
| Consistency | Platform conventions that adapt to context | Follow standard patterns, respect user preferences |
Liquid Glass combines transparency, reflection, refraction, and fluidity with a frosted aesthetic:
// 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)
When to use Liquid Glass:
When NOT to use:
Three-column layout for document-based and content-heavy apps:
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")
}
}
}
}
}
@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)
}
}
| Style | Use Case |
|---|---|
.automatic | Standard app windows |
.hiddenTitleBar | Content-focused (media players) |
.plain | Utility windows, panels |
.unified | Integrated toolbar appearance |
.unifiedCompact | Compact toolbar height |
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 { }
}
.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
Always implement these when applicable:
| Action | Shortcut | Implementation |
|---|---|---|
| New | ⌘N | .keyboardShortcut("n") |
| Open | ⌘O | .keyboardShortcut("o") |
| Save | ⌘S | .keyboardShortcut("s") |
| Close | ⌘W | .keyboardShortcut("w") |
| Undo | ⌘Z | .keyboardShortcut("z") |
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)
| Size | Shape | Use Case |
|---|---|---|
| Mini | Rounded rect | Compact toolbars, dense UIs |
| Small | Rounded rect | Secondary controls, sidebars |
| Regular | Rounded rect | Primary controls (default) |
| Large | Capsule | Prominent actions |
| Extra Large | Capsule + Glass | Hero CTAs, onboarding |
// 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.")
}
// 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)
// 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) }
}
// 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)
| Deprecated | Modern | Notes |
|---|---|---|
NavigationView | NavigationSplitView / NavigationStack | Split for macOS, Stack for simple flows |
.navigationViewStyle(.columns) | NavigationSplitView | Built-in column support |
List { }.listStyle(.sidebar) with NavigationLink |
// 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
})
.glassEffect() or appropriate materialNavigationSplitView used for multi-column layoutsDocumentGroup.borderedProminent.destructive role┌─────────────────────────────────────────────────────┐
│ 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)
Weekly Installs
49
Repository
GitHub Stars
1
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode47
codex47
gemini-cli44
cursor44
claude-code42
github-copilot42
前端打磨(Polish)终极指南:提升产品细节与用户体验的系统化检查清单
59,700 周安装
| Redo | ⇧⌘Z | .keyboardShortcut("z", modifiers: [.command, .shift]) |
| Cut | ⌘X | .keyboardShortcut("x") |
| Copy | ⌘C | .keyboardShortcut("c") |
| Paste | ⌘V | .keyboardShortcut("v") |
| Select All | ⌘A | .keyboardShortcut("a") |
| Find | ⌘F | .keyboardShortcut("f") |
| Preferences | ⌘, | .keyboardShortcut(",") |
| Hide | ⌘H | System handled |
| Quit | ⌘Q | System handled |
NavigationSplitView sidebar |
| Proper split view behavior |
.toolbar { ToolbarItem(...) } in detail | .toolbar on NavigationSplitView | Toolbar applies to correct scope |
NSWindowController | WindowGroup / Window | Pure SwiftUI window management |
NSMenu / NSMenuItem | .commands { } / CommandMenu | Declarative menus |
NSToolbar | .toolbar { } | SwiftUI toolbar API |
NSTouchBar | .touchBar { } | SwiftUI Touch Bar |
NSOpenPanel.begin() | .fileImporter() | SwiftUI file dialog |
NSSavePanel.begin() | .fileExporter() | SwiftUI save dialog |
.background(Color.clear) for materials | .background(.regularMaterial) | Proper material support |
| Custom blur effects | .glassEffect() (Tahoe) | Native Liquid Glass |