mcpField
I
ios-design-guidelines

ios-design-guidelines

github.com/ehmo设计与 UI6312.0k 次安装32 周前更新
代码仓库

关于 ios-design-guidelines

面向 iPhone 应用界面设计、评审和重构的指南 Skill,依据 Apple Human Interface Guidelines 汇集布局、导航、排版、颜色与无障碍规则。

主要功能

  1. 01布局与安全区域规范
  2. 02导航结构建议
  3. 03动态字体与排版规则
  4. 04明暗模式和颜色对比指导
  5. 05VoiceOver 等无障碍建议

适用场景

  1. 01设计新的 iPhone 应用界面
  2. 02检查触控区域与安全区域
  3. 03调整导航和屏幕尺寸适配
  4. 04审核动态字体及无障碍支持

安装

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

$npx skills add https://github.com/ehmo/platform-design-skills --skill ios-design-guidelines

安装后在 Claude Code 中输入/ios-design-guidelines确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
metadata.json
rules/
_sections.md

详细介绍

iPhone 设计指南

基于 Apple《人机界面指南》制定的全面规则。构建、审查或重构任何 iPhone 应用界面时,请遵循这些规则。


1. 布局与安全区域

影响: 严重

规则 1.1:触控目标最小为 44pt

所有可交互元素的点击目标至少为 44x44pt,包括按钮、链接、切换开关和自定义控件。

正确:

Button("Save") { save() }
    .frame(minWidth: 44, minHeight: 44)

错误:

// 20pt icon with no padding — too small to tap reliably
Button(action: save) {
    Image(systemName: "checkmark")
        .font(.system(size: 20))
}
// Missing .frame(minWidth: 44, minHeight: 44)
规则 1.2:遵循安全区域

切勿将可交互内容或重要内容放置在状态栏、灵动岛或主屏幕指示器下方。使用 SwiftUI 的自动安全区域处理,或使用 UIKit 的 safeAreaLayoutGuide。

正确:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Content")
        }
        // SwiftUI respects safe areas by default
    }
}

错误:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Content")
        }
        .ignoresSafeArea() // Content will be clipped under notch/Dynamic Island
    }
}

仅将 .ignoresSafeArea() 用于背景填充、图像或装饰元素,切勿用于文本或交互控件。

规则 1.3:将主要操作放在拇指可及区域

将主要操作放在屏幕底部,也就是用户拇指自然停留的位置。次要操作和导航应放在顶部。

正确:

VStack {
    ScrollView { /* content */ }
    Button("Continue") { next() }
        .buttonStyle(.borderedProminent)
        .padding()
}

错误:

VStack {
    Button("Continue") { next() } // Top of screen — hard to reach one-handed
        .buttonStyle(.borderedProminent)
        .padding()
    ScrollView { /* content */ }
}
规则 1.4:支持所有 iPhone 屏幕尺寸

设计需兼容宽度为 375pt 的 iPhone SE 至宽度为 430pt 的 iPhone Pro Max。使用灵活布局,避免硬编码宽度。

正确:

HStack(spacing: 12) {
    ForEach(items) { item in
        CardView(item: item)
            .frame(maxWidth: .infinity) // Adapts to screen width
    }
}

错误:

HStack(spacing: 12) {
    ForEach(items) { item in
        CardView(item: item)
            .frame(width: 180) // Breaks on SE, wastes space on Pro Max
    }
}
规则 1.5:遵循 8pt 网格对齐

间距、内边距和元素尺寸应为 8pt 的倍数(8、16、24、32、40、48)。精细调整时可使用 4pt。

规则 1.6:支持横屏

除非应用针对特定任务设计(例如相机),否则应支持横屏方向。使用 ViewThatFits 或 GeometryReader 实现自适应布局。


2. 导航

影响: 严重

规则 2.1:使用标签栏呈现顶层版块

在屏幕底部使用标签栏呈现 3 至 5 个顶层版块。每个标签应代表不同的内容类别或功能。

正确:

TabView {
    HomeView()
        .tabItem {
            Label("Home", systemImage: "house")
        }
    SearchView()
        .tabItem {
            Label("Search", systemImage: "magnifyingglass")
        }
    ProfileView()
        .tabItem {
            Label("Profile", systemImage: "person")
        }
}

错误:

// Hamburger menu hidden behind three lines — discoverability is near zero
NavigationView {
    Button(action: { showMenu.toggle() }) {
        Image(systemName: "line.horizontal.3")
    }
}
规则 2.2:切勿使用汉堡菜单

汉堡菜单(抽屉菜单)会隐藏导航、降低可发现性,并违背 iOS 惯例。请改用标签栏。如果版块超过 5 个,请整合版块或使用“更多”标签。

规则 2.3:在主视图中使用大标题

在顶层视图中使用 .navigationBarTitleDisplayMode(.large)。用户滚动时,标题会切换为行内模式(.inline)。

正确:

NavigationStack {
    List(items) { item in
        ItemRow(item: item)
    }
    .navigationTitle("Messages")
    .navigationBarTitleDisplayMode(.large)
}
规则 2.4:切勿覆盖返回手势

从左侧边缘向右滑动以返回,是用户对系统的预期行为。切勿附加会干扰此手势的自定义手势识别器。

错误:

.gesture(
    DragGesture()
        .onChanged { /* custom drawer */ } // Conflicts with system back swipe
)
规则 2.5:使用 NavigationStack 呈现层级内容

对于逐层深入的内容,请使用 NavigationStack(而非已弃用的 NavigationView)。使用 NavigationPath 实现程序化导航。

正确:

NavigationStack(path: $path) {
    List(items) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationDestination(for: Item.self) { item in
        ItemDetail(item: item)
    }
}
规则 2.6:在导航过程中保留状态

用户返回后再前进,或切换标签时,应恢复之前的滚动位置和输入状态。使用 @SceneStorage 或 @State 保留视图状态。

规则 2.7:优先让用户识别,而非回忆

让当前位置、最近选择和可用目的地保持可见。恢复标签、滚动、筛选和选择状态,让用户能够通过识别继续操作,而不必凭记忆重建上下文。


3. 排版与动态字体

影响: 高

规则 3.1:使用内置文本样式

始终使用语义文本样式,而非硬编码字号。这些样式会随动态字体自动缩放。

正确:

VStack(alignment: .leading, spacing: 4) {
    Text("Section Title")
        .font(.headline)
    Text("Body content that explains the section.")
        .font(.body)
    Text("Last updated 2 hours ago")
        .font(.caption)
        .foregroundStyle(.secondary)
}

错误:

VStack(alignment: .leading, spacing: 4) {
    Text("Section Title")
        .font(.system(size: 17, weight: .semibold)) // Won't scale with Dynamic Type
    Text("Body content")
        .font(.system(size: 15)) // Won't scale with Dynamic Type
}
规则 3.2:支持动态字体,包括辅助功能字号

动态字体在最大的辅助功能字号下可将文本放大至约 200%。布局必须能够重新排列,切勿截断或裁切重要文本。

正确:

HStack {
    Image(systemName: "star")
    Text("Favorites")
        .font(.body)
}
// At accessibility sizes, consider using ViewThatFits or
// AnyLayout to switch from HStack to VStack

使用 @Environment(\.dynamicTypeSize) 检测字号类别并调整布局:

@Environment(\.dynamicTypeSize) var dynamicTypeSize

var body: some View {
    if dynamicTypeSize.isAccessibilitySize {
        VStack { content }
    } else {
        HStack { content }
    }
}
规则 3.3:自定义字体必须随动态字体调整缩放

如果使用自定义字体,必须对其进行缩放,使其能够响应动态字体调整。不同框架使用的 API 各不相同。

正确(SwiftUI):

extension Font {
    static func scaledCustom(size: CGFloat, relativeTo textStyle: Font.TextStyle) -> Font {
        .custom("CustomFont-Regular", size: size, relativeTo: textStyle)
    }
}

// Usage
Text("Hello")
    .font(.scaledCustom(size: 17, relativeTo: .body))

正确(UIKit):

let metrics = UIFontMetrics(forTextStyle: .body)
let customFont = UIFont(name: "CustomFont-Regular", size: 17)!
label.font = metrics.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
规则 3.4:将 SF Pro 作为系统字体

除非品牌要求另有规定,否则请使用系统字体(SF Pro)。SF Pro 针对 Apple 显示屏的易读性进行了优化。

规则 3.5:文本最小为 11pt

文本显示尺寸不得小于 11pt。正文优先使用 17pt。caption2 样式(11pt)是绝对最小值。

规则 3.6:通过字重和字号建立层级

通过字体字重和字号建立视觉层级。区分不同文本层级时,不要只依赖颜色。


4. 颜色与深色模式

**影响:**高

规则 4.1:使用语义系统颜色

使用系统提供的语义颜色,使其能够自动适配浅色和深色模式。

正确:

Text("Primary text")
    .foregroundStyle(.primary) // Adapts to light/dark

Text("Secondary info")
    .foregroundStyle(.secondary)

VStack { }
    .background(Color(.systemBackground)) // White in light, black in dark

错误:

Text("Primary text")
    .foregroundColor(.black) // Invisible on dark backgrounds

VStack { }
    .background(.white) // Blinding in Dark Mode
规则 4.2:为自定义颜色提供浅色和深色变体

在资源目录中定义自定义颜色,并同时提供任意外观和深色外观变体。

// In Assets.xcassets, define "BrandBlue" with:
// Any Appearance: #0066CC
// Dark Appearance: #4DA3FF

Text("Brand text")
    .foregroundStyle(Color("BrandBlue")) // Automatically switches
规则 4.3:不要只依赖颜色传达信息

始终搭配文本、图标或形状来传达含义。约有 8% 的男性存在某种形式的色觉缺陷。

正确:

HStack {
    Image(systemName: "exclamationmark.triangle.fill")
        .foregroundStyle(.red)
    Text("Error: Invalid email address")
        .foregroundStyle(.red)
}

错误:

// Only color indicates the error — invisible to colorblind users
TextField("Email", text: $email)
    .border(isValid ? .green : .red)
规则 4.4:对比度至少为 4.5:1

所有文本都必须符合 WCAG AA 对比度要求:普通文本为 4.5:1,大号文本(18pt 及以上,或粗体 14pt 及以上)为 3:1。

规则 4.5:支持 Display P3 广色域

在现代 iPhone 上使用 Display P3 色彩空间,呈现鲜艳、准确的色彩。在资源目录中使用 Display P3 色域定义颜色。

规则 4.6:背景层级

使用三级背景层级营造层次感:

  • systemBackground — 主要表面
  • secondarySystemBackground — 分组内容、卡片
  • tertiarySystemBackground — 分组内容中的元素
规则 4.7:交互元素使用一种强调色

为所有交互元素(按钮、链接、开关)选择一种色调/强调色。这有助于建立一致、易于理解的视觉语言。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .tint(.indigo) // All interactive elements use indigo
        }
    }
}

5. 辅助功能

**影响:**极高

规则 5.1:为所有交互元素添加 VoiceOver 标签

每个按钮、控件和交互元素都必须有含义明确的辅助功能标签。

正确:

Button(action: addToCart) {
    Image(systemName: "cart.badge.plus")
}
.accessibilityLabel("Add to cart")

错误:

Button(action: addToCart) {
    Image(systemName: "cart.badge.plus")
}
// VoiceOver reads "cart.badge.plus" — meaningless to users
规则 5.2:设置符合逻辑的 VoiceOver 导航顺序

确保 VoiceOver 按照符合逻辑的顺序朗读元素。如果视觉布局与朗读顺序不一致,请使用 .accessibilitySortPriority() 调整顺序。

VStack {
    Text("Price: $29.99")
        .accessibilitySortPriority(1) // Read second (lower number = lower priority)
    Text("Product Name")
        .accessibilitySortPriority(2) // Read first (higher number = higher priority)
}
规则 5.3:支持粗体文本

用户在“设置”中启用粗体文本后,使用自定义方式渲染的文本必须相应调整。SwiftUI 文本样式会自动处理此项。对于 SwiftUI 自定义渲染,请使用 @Environment(\.legibilityWeight) 应用更粗的字重。UIKit 代码必须检查 UIAccessibility.isBoldTextEnabled,并在 UIAccessibility.boldTextStatusDidChangeNotification 通知时重新查询。

正确:

// SwiftUI — standard text styles adapt automatically
Text("Section Header")
    .font(.headline)

// SwiftUI — custom rendering respects legibilityWeight
@Environment(\.legibilityWeight) var legibilityWeight

var body: some View {
    Text("Custom Label")
        .fontWeight(legibilityWeight == .bold ? .bold : .regular)
}

错误:

// Hardcoded weight ignores Bold Text preference
label.font = UIFont.systemFont(ofSize: 17, weight: .regular)
// Missing: re-query font when UIAccessibility.boldTextStatusDidChangeNotification fires
规则 5.4:支持减弱动态效果

启用“减弱动态效果”后,请禁用装饰性动画和视差效果。使用 @Environment(\.accessibilityReduceMotion)。

正确:

@Environment(\.accessibilityReduceMotion) var reduceMotion

var body: some View {
    CardView()
        .animation(reduceMotion ? nil : .spring(), value: isExpanded)
}
规则 5.5:支持提高对比度

用户启用“提高对比度”后,确保自定义颜色提供对比度更高的变体。使用 @Environment(\.colorSchemeContrast) 进行检测。

规则 5.6:不要只通过颜色、形状或位置传达信息

信息必须通过多种方式提供。为视觉指示器配上文本或辅助功能说明。

规则 5.7:为所有手势提供替代交互方式

每个自定义手势都必须提供等效的点按或菜单操作,供无法完成复杂手势的用户使用。

规则 5.8:支持切换控制和完整键盘访问

确保所有交互都能通过切换控制(外接开关)和完整键盘访问(蓝牙键盘)使用。测试导航顺序和焦点行为。


6. 手势与输入

影响:高

规则 6.1:使用标准手势

使用标准的 iOS 手势术语:轻点、长按、滑动、捏合、旋转。用户已经熟悉这些手势。

手势标准用途
轻点主要操作、选择
长按上下文菜单、预览
水平滑动删除、归档、返回导航
垂直滑动滚动、关闭工作表
捏合放大/缩小
双指旋转旋转内容
规则 6.2:绝不覆盖系统手势

以下手势由系统保留,绝不能拦截:

  • 从左侧边缘向右滑动(返回导航)
  • 从左上角向下滑动(通知中心)
  • 从右上角向下滑动(控制中心)
  • 从底部向上滑动(主屏幕/应用切换器)
规则 6.3:自定义手势必须易于发现

如果添加自定义手势,应提供视觉提示(例如抓手),并确保也能通过可见按钮或菜单项使用该操作。

规则 6.4:支持所有输入方式

优先为触控设计,同时也要支持:

  • 硬件键盘(iPad 键盘配件、蓝牙键盘)
  • 辅助设备(切换控制、头部追踪)
  • 指针输入(辅助触控)

7. 组件

影响:高

规则 7.1:按钮样式

恰当使用内置按钮样式:

  • .borderedProminent — 主要行动号召
  • .bordered — 次要操作
  • .borderless — 第三级或行内操作
  • .destructive 角色 — 删除/移除时使用红色着色

正确:

VStack(spacing: 16) {
    Button("Purchase") { buy() }
        .buttonStyle(.borderedProminent)

    Button("Add to Wishlist") { wishlist() }
        .buttonStyle(.bordered)

    Button("Delete", role: .destructive) { delete() }
}
规则 7.2:警告框——仅用于关键信息

仅在需要用户决策的关键信息场景中谨慎使用警告框。优先使用 2 个按钮,最多 3 个。破坏性选项应使用 .destructive 角色。

正确:

.alert("Delete Photo?", isPresented: $showAlert) {
    Button("Delete", role: .destructive) { deletePhoto() }
    Button("Cancel", role: .cancel) { }
} message: {
    Text("This photo will be permanently removed.")
}

错误:

// Alert for non-critical info — should be a banner or toast
.alert("Tip", isPresented: $showTip) {
    Button("OK") { }
} message: {
    Text("Swipe left to delete items.")
}
规则 7.3:使用工作表处理独立任务

使用工作表呈现独立任务。始终提供关闭方式(关闭按钮或向下滑动)。半高工作表使用 .presentationDetents()。

.sheet(isPresented: $showCompose) {
    NavigationStack {
        ComposeView()
            .navigationTitle("New Message")
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") { showCompose = false }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("Send") { send() }
                }
            }
    }
    .presentationDetents([.medium, .large])
}
规则 7.4:列表——默认使用内嵌分组样式

默认使用 .insetGrouped 列表样式。为常见操作支持滑动操作。行高至少为 44pt。

正确:

List {
    Section("Recent") {
        ForEach(recentItems) { item in
            ItemRow(item: item)
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive) { delete(item) } label: {
                        Label("Delete", systemImage: "trash")
                    }
                    Button { archive(item) } label: {
                        Label("Archive", systemImage: "archivebox")
                    }
                    .tint(.blue)
                }
        }
    }
}
.listStyle(.insetGrouped)
规则 7.5:标签栏行为
  • 标签图标使用 SF Symbols——选中标签使用填充样式,未选中标签使用轮廓样式
  • 在标签内深入导航时,绝不隐藏标签栏
  • 使用 .badge() 标记重要数量
TabView {
    MessagesView()
        .tabItem {
            Label("Messages", systemImage: "message")
        }
        .badge(unreadCount)
}
规则 7.6:搜索

使用 .searchable() 放置搜索框。提供搜索建议并支持最近搜索记录。

NavigationStack {
    List(filteredItems) { item in
        ItemRow(item: item)
    }
    .searchable(text: $searchText, prompt: "Search items")
    .searchSuggestions {
        ForEach(suggestions) { suggestion in
            Text(suggestion.title)
                .searchCompletion(suggestion.title)
        }
    }
}
规则 7.7:上下文菜单

使用上下文菜单(长按)提供次要操作。绝不能将上下文菜单作为访问某项操作的唯一方式。

PhotoView(photo: photo)
    .contextMenu {
        Button { share(photo) } label: {
            Label("Share", systemImage: "square.and.arrow.up")
        }
        Button { favorite(photo) } label: {
            Label("Favorite", systemImage: "heart")
        }
        Button(role: .destructive) { delete(photo) } label: {
            Label("Delete", systemImage: "trash")
        }
    }
规则 7.8:进度指示器
  • 对持续时间已知的操作,使用确定进度(ProgressView(value:total:))
  • 对持续时间未知的操作,使用不确定进度(ProgressView())
  • 绝不使用旋转指示器阻塞整个屏幕
规则 7.9:SF Symbols——渲染模式

为每个符号使用恰当的渲染模式。默认使用单色;在适当场景下,分层、调色板和多色模式能带来更丰富的表达。始终优先选择最能传达含义的符号渲染模式——当多色能表达关键状态时,不要默认使用单色。

正确:

// Hierarchical: single color with automatic opacity layers
Image(systemName: "person.crop.circle.fill")
    .symbolRenderingMode(.hierarchical)
    .foregroundStyle(.blue)

// Multicolor: system-defined color per layer (e.g., battery, weather)
Image(systemName: "battery.100percent.bolt")
    .symbolRenderingMode(.multicolor)

// Palette: explicit per-layer colors
Image(systemName: "folder.badge.plus")
    .symbolRenderingMode(.palette)
    .foregroundStyle(.white, .blue)

错误:

// Monochrome on a symbol that has meaningful multicolor layers
Image(systemName: "battery.100percent.bolt")
    .foregroundColor(.gray) // loses the contextual color meaning
规则 7.10:SF Symbols——字重与缩放

符号字重应与相邻文本的字重一致。使用缩放变体(.small、.medium、.large),而不是调整大小。符号看起来绝不能比相邻文本更粗。

正确:

Label("Download", systemImage: "arrow.down.circle.fill")
    .font(.body.weight(.semibold))
    // Symbol inherits .semibold weight automatically via Label

错误:

HStack {
    Image(systemName: "arrow.down.circle.fill")
        .font(.system(size: 32)) // explicit size ignores type scale
    Text("Download")
        .font(.body)
}
规则 7.11:SF Symbols——动画(iOS 17+)

使用 symbolEffect 实现符号状态转换。操作优先使用离散效果(.bounce、.pulse),持续状态优先使用无限效果(.variableColor)。如果可使用 contentTransition(.symbolEffect),不要在符号名称之间手动交叉淡入淡出。

正确:

Image(systemName: isLoading ? "arrow.2.circlepath" : "checkmark.circle")
    .contentTransition(.symbolEffect(.replace))
    .symbolEffect(.pulse, isActive: isLoading)

错误:

// Manual opacity cross-fade between symbol names
if isLoading {
    Image(systemName: "arrow.2.circlepath")
} else {
    Image(systemName: "checkmark.circle")
}

8. 设计模式

影响: 中

规则 8.1:新手引导 — 最多 3 页,且可跳过

新手引导不超过 3 页,并始终提供跳过选项。推迟到用户需要使用需认证的功能时再要求登录。

TabView {
    OnboardingPage(
        image: "wand.and.stars",
        title: "Smart Suggestions",
        subtitle: "Get personalized recommendations based on your preferences."
    )
    OnboardingPage(
        image: "bell.badge",
        title: "Stay Updated",
        subtitle: "Receive notifications for things that matter to you."
    )
    OnboardingPage(
        image: "checkmark.shield",
        title: "Private & Secure",
        subtitle: "Your data stays on your device."
    )
}
.tabViewStyle(.page)
.overlay(alignment: .topTrailing) {
    Button("Skip") { completeOnboarding() }
        .padding()
}
规则 8.2:加载 — 使用骨架视图,不使用阻塞式转圈指示器

使用与待加载内容布局相符的骨架视图或占位视图。切勿显示全屏阻塞式转圈指示器。

正确:

if isLoading {
    ForEach(0..<5) { _ in
        SkeletonRow() // Placeholder matching final row layout
            .redacted(reason: .placeholder)
    }
} else {
    ForEach(items) { item in
        ItemRow(item: item)
    }
}

错误:

if isLoading {
    ProgressView("Loading...") // Blocks the entire view
} else {
    List(items) { item in ItemRow(item: item) }
}
规则 8.3:启动屏幕 — 与首屏保持一致

启动故事板在视觉上必须与应用的初始屏幕一致。不要显示启动徽标或品牌宣传屏幕。这样能让用户感觉应用瞬间启动。

规则 8.4:模态视图 — 谨慎使用

仅在用户必须完成或放弃一项专注任务时才显示模态视图。始终提供明确的关闭操作。切勿在模态视图上再叠加模态视图。

规则 8.5:通知 — 只发送高价值通知

仅针对用户真正关心的内容发送通知。支持可执行操作的通知。对通知进行分类,以便用户精细控制。

规则 8.6:设置入口
  • 常用设置: 放在应用内设置屏幕,可通过个人资料图标或齿轮图标访问
  • 隐私/权限设置: 通过 URL 方案跳转到系统“设置”应用
  • 切勿在应用内重复提供系统级控制项
规则 8.7:反馈 — 视觉与触觉

对每项用户操作立即提供反馈:

  • 视觉状态变化(按钮高亮、动画)
  • 对重要操作使用 UIImpactFeedbackGenerator、UINotificationFeedbackGenerator 或 UISelectionFeedbackGenerator 提供触觉反馈
Button("Complete") {
    let generator = UINotificationFeedbackGenerator()
    generator.notificationOccurred(.success)
    completeTask()
}
规则 8.8:立即显示等待状态

如果某项操作无法立即完成,应立刻响应用户的点击,然后显示行内进度、骨架视图或部分结果。任务仍在进行时,切勿让界面看起来毫无变化。


9. 隐私与权限

影响: 高

规则 9.1:在适当场景请求权限

在用户执行需要权限的操作时请求权限,切勿在应用启动时请求。

正确:

Button("Take Photo") {
    // Request camera permission only when the user taps this button
    AVCaptureDevice.requestAccess(for: .video) { granted in
        if granted { showCamera = true }
    }
}

错误:

// In AppDelegate.didFinishLaunching — too early, no context
func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) {
    AVCaptureDevice.requestAccess(for: .video) { _ in }
    CLLocationManager().requestWhenInUseAuthorization()
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { _, _ in }
}
规则 9.2:显示系统提示前先作说明

触发系统权限对话框前,先显示自定义说明屏幕。系统对话框只会出现一次;如果用户拒绝,应用必须引导用户前往“设置”。

struct LocationExplanation: View {
    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "location.fill")
                .font(.largeTitle)
            Text("Find Nearby Stores")
                .font(.headline)
            Text("We use your location to show stores within walking distance. Your location is never shared or stored.")
                .font(.body)
                .multilineTextAlignment(.center)
            Button("Enable Location") {
                locationManager.requestWhenInUseAuthorization()
            }
            .buttonStyle(.borderedProminent)
            Button("Not Now") { dismiss() }
                .foregroundStyle(.secondary)
        }
        .padding()
    }
}
规则 9.3:支持“通过 Apple 登录”

如果应用提供任何第三方登录方式(Google、Facebook),也必须提供“通过 Apple 登录”。并将其作为首选项。

规则 9.4:非必要不要求创建账户

允许用户先体验应用,再要求登录。仅对确实需要身份验证的功能(购买、同步、社交功能)设置访问限制。

规则 9.5:App 跟踪透明度

如果要跨应用或网站跟踪用户,必须显示 ATT 提示。尊重用户的拒绝;对于选择退出的用户,不要降低使用体验。

规则 9.6:一次性访问使用定位按钮

对于只需获取一次位置、无需请求持续权限的操作,请使用 LocationButton。

import CoreLocationUI

LocationButton(.currentLocation) {
    fetchNearbyStores()
}
.labelStyle(.titleAndIcon)

10. 系统集成

影响: 中

规则 10.1:使用小组件展示一目了然的数据

使用 WidgetKit 提供小组件,展示用户经常查看的信息,并显示最实用的快照。从 iOS 17 开始,小组件支持交互式控件:使用由 App Intents 支持的 Button 和 Toggle,让用户无需打开应用即可直接通过小组件执行操作。

// iOS 17+ interactive widget with a Button
struct TimerWidgetView: View {
    let entry: TimerEntry

    var body: some View {
        VStack {
            Text(entry.remaining, style: .timer)
                .font(.title2.bold())
            Button(intent: ToggleTimerIntent()) {
                Label(entry.isRunning ? "Pause" : "Start",
                      systemImage: entry.isRunning ? "pause.fill" : "play.fill")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
规则 10.2:使用 App Shortcuts 执行关键操作

定义 App Shortcuts,让用户能够通过 Siri、Spotlight 和“快捷指令”应用触发关键操作。

struct MyAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: StartWorkoutIntent(),
            phrases: ["Start a workout in \(.applicationName)"],
            shortTitle: "Start Workout",
            systemImageName: "figure.run"
        )
    }
}
规则 10.3:Spotlight 索引

使用 CSSearchableItem 为应用内容建立索引,让用户能够通过 Spotlight 搜索找到内容。

规则 10.4:集成分享表单

对于用户可能想要分享出去的内容,支持系统分享表单。实现 UIActivityItemSource,或在 SwiftUI 中使用 ShareLink。

ShareLink(item: article.url) {
    Label("Share", systemImage: "square.and.arrow.up")
}
规则 10.5:实时活动

使用实时活动和灵动岛显示实时、有时限的事件(配送追踪、体育比分、锻炼)。

规则 10.6:优雅地处理中断

遇到以下中断时,保存状态并优雅地暂停:

  • 电话
  • Siri 调用
  • 通知
  • App 切换器
  • FaceTime SharePlay

使用 scenePhase 检测状态变化:

@Environment(\.scenePhase) var scenePhase

.onChange(of: scenePhase) { _, newPhase in
    switch newPhase {
    case .active: resumeActivity()
    case .inactive: pauseActivity()
    case .background: saveState()
    @unknown default: break
    }
}

快速参考

需求组件说明
顶层分区(3-5 个)TabView with .tabItem底部标签栏,SF Symbols
层级式深入浏览NavigationStack根视图使用大标题,子视图使用内联标题
独立任务.sheet滑动关闭,提供取消/完成按钮
关键决策.alert首选 2 个按钮,最多 3 个
次要操作.contextMenu长按触发;也必须能从其他位置访问
可滚动内容List with .insetGrouped行高至少 44pt,支持滑动操作
文本输入TextField / TextEditor上方显示标签,下方显示验证信息
选择(少量选项)Picker2-5 个选项使用分段控件,更多选项使用滚轮
选择(开/关)Toggle在列表行中右对齐
搜索.searchable提供建议和最近搜索
进度(已知)ProgressView(value:total:)显示百分比或剩余时间
进度(未知)ProgressView()内嵌显示,切勿阻塞全屏
单次定位LocationButton无需持久权限
分享内容ShareLink系统分享面板
触觉反馈UIImpactFeedbackGenerator.light、.medium、.heavy
破坏性操作Button(role: .destructive)红色着色,通过警告框确认

评估清单

使用此清单检查 iPhone 应用是否符合 HIG:

布局与安全区域
  • 所有触控目标至少为 44x44pt
  • 状态栏、灵动岛或主屏幕指示器下方没有被裁切的内容
  • 主要操作位于屏幕下半部分(拇指操作区)
  • 布局可适配从 iPhone SE 到 Pro Max 的机型,不会错乱
  • 间距遵循 8pt 网格
导航
  • 使用标签栏呈现 3-5 个顶层分区
  • 不使用汉堡菜单/抽屉菜单
  • 主要视图使用大标题
  • 全程支持从屏幕左边缘滑动返回
  • 切换标签页时保留状态
排版
  • 所有文本使用内置文本样式,或使用支持 Dynamic Type 缩放的自定义字体(SwiftUI 中使用 Font.custom(_:size:relativeTo:),或 UIKit 中使用 UIFontMetrics)
  • 支持 Dynamic Type,最高适配辅助功能字号
  • 大字号下布局可重新排列(重要文本不会被截断)
  • 最小字号为 11pt
颜色与深色模式
  • 应用使用语义化系统颜色,或提供浅色/深色资源变体
  • 深色模式经过精心设计(而非简单反色)
  • 不会仅通过颜色传递信息
  • 文本对比度达到 4.5:1(普通文本)或 3:1(大号文本)
  • 交互元素使用单一强调色
辅助功能
  • VoiceOver 能以合理顺序朗读所有屏幕,并提供有意义的标签
  • 遵循粗体文本偏好设置
  • 减弱动态效果设置会禁用装饰性动画
  • 自定义颜色提供增强对比度变体
  • 所有手势都提供替代访问方式
组件
  • 仅在关键决策时使用警告框
  • 表单提供关闭方式(按钮和/或滑动)
  • 列表行至少高 44pt
  • 导航过程中绝不隐藏标签栏
  • 破坏性按钮使用 .destructive 角色
隐私
  • 在适当场景请求权限,而非在启动时请求
  • 每次显示系统权限对话框前,都先显示自定义说明
  • 除其他服务提供商外,也提供使用 Apple 登录
  • 无需账户即可使用应用的基本功能
  • 如果进行跟踪,则显示 ATT 提示,并尊重用户的拒绝
系统集成
  • 小组件显示一目了然且保持最新的信息
  • App 内容已编入 Spotlight 索引
  • 可通过共享表单分享可共享的内容
  • App 能妥善处理打断(通话、切换到后台、Siri)

反模式

以下是违反 iOS 人机界面指南的常见错误。请勿这样做:

  1. 汉堡菜单 — 使用标签栏。汉堡菜单会隐藏导航,并使功能的可发现性降低多达 50%。

  2. 会导致侧滑返回失效的自定义返回按钮 — 如果替换返回按钮,请确保通过 NavigationStack 仍可使用从左侧边缘向右滑动的手势。

  3. 全屏阻塞式加载指示器 — 使用骨架视图或内联进度指示器。阻塞式加载指示器会让 App 显得像是卡住了。

  4. 带 Logo 的启动屏幕 — 启动屏幕必须与 App 的首屏一致。启动时展示品牌信息会显得不自然。

  5. 启动时请求所有权限 — 首次启动时就请求相机、位置、通知和通讯录权限,基本上都会被拒绝。

  6. 硬编码字体大小 — 使用文本样式。硬编码大小会忽略动态字体和辅助功能偏好,让数百万用户无法正常使用 App。

  7. 只用颜色表示状态 — 用红色和绿色表示有效或无效会让色盲用户无法区分。务必同时搭配图标或文字。

  8. 用警告框展示非关键信息 — 警告框会打断流程,还需要用户手动关闭。提示和非关键信息应使用横幅、轻提示或内联消息。

  9. 推送页面时隐藏标签栏 — 在标签页内导航时,标签栏应始终显示。隐藏标签栏会让用户不知所措。

  10. 忽略安全区域 — 在内容视图上使用 .ignoresSafeArea() 会导致文本和按钮被刘海、灵动岛或主屏幕指示条遮挡。

  11. 无法关闭的模态界面 — 每个模态界面都必须提供明确的关闭方式(关闭按钮、取消或向下轻扫)。将用户困在模态界面中是不友好的。

  12. 没有替代操作方式的自定义手势 — 三指轻扫撤销对很多人来说并不可用。还应提供可见按钮或菜单项。

  13. 过小的触控目标 — 小于 44pt 的按钮和链接容易造成误触,尤其是在列表和工具栏中。

  14. 层叠模态界面 — 在一个工作表上再展示一个工作表,又叠加一个工作表,会让导航变得混乱。应在单个模态界面中使用导航。

  15. 事后才考虑深色模式 — 使用硬编码颜色会导致 App 在深色模式或浅色模式下无法正常显示。务必使用语义颜色。

评论

支持 Markdown