关于 ios-design-guidelines
面向 iPhone 应用界面设计、评审和重构的指南 Skill,依据 Apple Human Interface Guidelines 汇集布局、导航、排版、颜色与无障碍规则。
主要功能
- 01布局与安全区域规范
- 02导航结构建议
- 03动态字体与排版规则
- 04明暗模式和颜色对比指导
- 05VoiceOver 等无障碍建议
适用场景
- 01设计新的 iPhone 应用界面
- 02检查触控区域与安全区域
- 03调整导航和屏幕尺寸适配
- 04审核动态字体及无障碍支持
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ehmo/ platform-design-skills --skill ios-design-guidelines安装后在 Claude Code 中输入/ios-design-guidelines确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonrules/_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 | 上方显示标签,下方显示验证信息 |
| 选择(少量选项) | Picker | 2-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 人机界面指南的常见错误。请勿这样做:
-
汉堡菜单 — 使用标签栏。汉堡菜单会隐藏导航,并使功能的可发现性降低多达 50%。
-
会导致侧滑返回失效的自定义返回按钮 — 如果替换返回按钮,请确保通过
NavigationStack仍可使用从左侧边缘向右滑动的手势。 -
全屏阻塞式加载指示器 — 使用骨架视图或内联进度指示器。阻塞式加载指示器会让 App 显得像是卡住了。
-
带 Logo 的启动屏幕 — 启动屏幕必须与 App 的首屏一致。启动时展示品牌信息会显得不自然。
-
启动时请求所有权限 — 首次启动时就请求相机、位置、通知和通讯录权限,基本上都会被拒绝。
-
硬编码字体大小 — 使用文本样式。硬编码大小会忽略动态字体和辅助功能偏好,让数百万用户无法正常使用 App。
-
只用颜色表示状态 — 用红色和绿色表示有效或无效会让色盲用户无法区分。务必同时搭配图标或文字。
-
用警告框展示非关键信息 — 警告框会打断流程,还需要用户手动关闭。提示和非关键信息应使用横幅、轻提示或内联消息。
-
推送页面时隐藏标签栏 — 在标签页内导航时,标签栏应始终显示。隐藏标签栏会让用户不知所措。
-
忽略安全区域 — 在内容视图上使用
.ignoresSafeArea()会导致文本和按钮被刘海、灵动岛或主屏幕指示条遮挡。 -
无法关闭的模态界面 — 每个模态界面都必须提供明确的关闭方式(关闭按钮、取消或向下轻扫)。将用户困在模态界面中是不友好的。
-
没有替代操作方式的自定义手势 — 三指轻扫撤销对很多人来说并不可用。还应提供可见按钮或菜单项。
-
过小的触控目标 — 小于 44pt 的按钮和链接容易造成误触,尤其是在列表和工具栏中。
-
层叠模态界面 — 在一个工作表上再展示一个工作表,又叠加一个工作表,会让导航变得混乱。应在单个模态界面中使用导航。
-
事后才考虑深色模式 — 使用硬编码颜色会导致 App 在深色模式或浅色模式下无法正常显示。务必使用语义颜色。