ipados-design-guidelines by ehmo/platform-design-skills
npx skills add https://github.com/ehmo/platform-design-skills --skill ipados-design-guidelines遵循苹果人机界面指南构建 iPad 原生应用的综合规则。iPad 不是大号 iPhone —— 它需要自适应布局、多任务支持、指针交互、键盘快捷键以及应用间拖放。这些规则扩展了 iOS 模式,以适应更大、功能更强的画布。
iPad 提供两种水平尺寸类别:常规(全屏、大分屏)和紧凑(侧拉、窄分屏)。为两者进行设计。切勿硬编码尺寸。
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .regular {
TwoColumnLayout()
} else {
StackedLayout()
}
}
}
iPad 布局必须专门构建。将 iPhone 布局拉伸到 13 英寸屏幕上会浪费空间且感觉不对。在常规宽度下,使用多列布局、主从模式并提高信息密度。
为全系列设计:iPad Mini (8.3")、iPad (11")、iPad Air (11"/13") 和 iPad Pro (11"/13")。使用重新分配内容的灵活布局,而不是简单地缩放。
在常规宽度下,将内容组织成列。两列布局最常见(侧边栏 + 详情)。三列布局适用于深层层次结构(侧边栏 + 列表 + 详情)。避免在大屏幕上使用单列全宽布局。
struct ThreeColumnLayout: View {
var body: some View {
NavigationSplitView {
SidebarView()
} content: {
ContentListView()
} detail: {
DetailView()
}
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
iPad 的安全区域与 iPhone 不同。旧款 iPad 没有主屏幕指示器。横屏下的 iPad 与竖屏下的内边距不同。始终使用 safeAreaInset,切勿为刘海或指示器硬编码内边距。
iPad 应用必须在横屏和竖屏下都能良好工作。横屏是生产力的主导方向。竖屏常用于阅读。根据方向调整列数和布局密度。
你的应用必须在分屏浏览的 1/3、1/2 和 2/3 屏幕宽度下正常运行。在 1/3 宽度下,你的应用会收到紧凑的水平尺寸类别。内容必须在每个分屏比例下都保持可用。
侧拉将你的应用作为紧凑宽度的覆盖层显示在右侧边缘。它的行为类似于 iPhone 宽度的应用。确保所有功能在此窄模式下都可访问。
台前调度允许自由调整大小的窗口和同时存在的多个窗口。你的应用必须:
流畅地调整到任意尺寸
支持显示不同内容的多个场景(窗口)
不假定任何固定尺寸或宽高比
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } // 支持多个窗口 WindowGroup("Detail", for: Item.ID.self) { $itemId in DetailView(itemId: itemId) } } }
应用可能直接启动进入分屏浏览或台前调度。在设置、引导或任何流程中,不要依赖全屏尺寸。在每个可能的尺寸下测试你的应用。
当用户通过多任务调整大小时,平滑地动画化布局变化。在尺寸转换过程中保持滚动位置、选择状态和用户上下文。切勿在调整大小时重新加载内容。
使用 UIScene / SwiftUI WindowGroup 让用户打开显示不同内容的多个应用实例。每个场景都是独立的。支持 NSUserActivity 以进行状态恢复。
在常规宽度下,用侧边栏替换 iPhone 标签栏。侧边栏为导航项提供了更多空间,支持分区,并且在 iPad 上感觉原生。
struct AppNavigation: View {
@State private var selection: NavigationItem? = .inbox
var body: some View {
NavigationSplitView {
List(selection: $selection) {
Section("Main") {
Label("Inbox", systemImage: "tray")
.tag(NavigationItem.inbox)
Label("Drafts", systemImage: "doc")
.tag(NavigationItem.drafts)
Label("Sent", systemImage: "paperplane")
.tag(NavigationItem.sent)
}
Section("Labels") {
// 动态分区
}
}
.navigationTitle("Mail")
} detail: {
DetailView(for: selection)
}
}
}
带有 .sidebarAdaptable 样式的 SwiftUI TabView 在常规宽度下会自动转换为侧边栏。使用此功能实现无缝的 iPhone 到 iPad 适配。
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Search", systemImage: "magnifyingglass") { SearchView() }
Tab("Profile", systemImage: "person") { ProfileView() }
}
.tabViewStyle(.sidebarAdaptable)
当你的信息架构有三个层级时,使用三列的 NavigationSplitView:类别 > 列表 > 详情。例如:邮件(账户 > 消息 > 消息)、文件管理器、设置。
在 iPad 上,工具栏位于屏幕顶部的导航栏区域,而不是像 iPhone 那样在底部。将上下文操作放在 .toolbar 中,并采用适当的放置位置。
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Compose", systemImage: "square.and.pencil") { }
}
ToolbarItemGroup(placement: .secondaryAction) {
Button("Archive", systemImage: "archivebox") { }
Button("Delete", systemImage: "trash") { }
}
}
当列表/侧边栏中没有选择任何项目时,在详情区域显示有意义的空状态。使用带有图标和说明文字的占位符,而不是空白屏幕。
保持侧边栏选择、搜索词和展开状态在尺寸变化和场景切换中可见并得以保留。在多列布局中,用户应从屏幕上的结构恢复,而不是依靠记忆。
所有可点击元素都应对指针悬停做出响应。系统为标准控件提供自动悬停效果。对于自定义视图,使用 .hoverEffect()。
Button("Action") { }
.hoverEffect(.highlight) // 悬停时轻微高亮
// 自定义悬停效果
MyCustomView()
.hoverEffect(.lift) // 抬起并添加阴影
指针应吸附到(被吸引向)按钮边界。标准的 UIKit/SwiftUI 按钮会自动获得此功能。对于自定义点击目标,使用 .contentShape() 确保指针区域与可点击区域匹配。
右键单击(辅助点击)应显示上下文菜单。使用 .contextMenu,它自动支持长按(触摸)和右键单击(指针)。
Text(item.title)
.contextMenu {
Button("Copy", systemImage: "doc.on.doc") { }
Button("Share", systemImage: "square.and.arrow.up") { }
Divider()
Button("Delete", systemImage: "trash", role: .destructive) { }
}
支持带惯性的双指滚动。在适当的地方支持捏合缩放。尊重滚动方向偏好。对于自定义滚动视图,确保触控板手势与触摸手势一起感觉自然。
根据上下文改变光标外观。文本区域显示 I 型光标。链接显示指针手形。调整大小手柄显示调整大小光标。可拖拽项目显示抓取光标。
指针用户期望通过点击并拖拽来重新排列、选择和移动内容。结合 Shift-单击和 Cmd-单击进行多选。
每个主要操作都必须有键盘快捷键。标准快捷键是强制性的:
| 快捷键 | 操作 |
|---|---|
| Cmd+N | 新建项目 |
| Cmd+F | 查找/搜索 |
| Cmd+S | 保存 |
| Cmd+Z | 撤销 |
| Cmd+Shift+Z | 重做 |
| Cmd+C/V/X | 复制/粘贴/剪切 |
| Cmd+A | 全选 |
| Cmd+P | 打印 |
| Cmd+W | 关闭窗口/标签页 |
| Cmd+, | 设置/偏好设置 |
| Delete | 删除所选项目 |
Button("New Document") { createDocument() }
.keyboardShortcut("n", modifiers: .command)
当用户按住 Cmd 键时,iPadOS 会显示一个快捷键叠加层。使用 .keyboardShortcut() 注册所有快捷键,以便它们出现在此叠加层中。按逻辑对相关快捷键进行分组。
支持 Tab 键向前移动,Shift+Tab 向后移动,在表单字段和可聚焦元素之间切换。使用 .focusable() 和 @FocusState 来管理键盘焦点顺序。
struct FormView: View {
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
TextField("Phone", text: $phone)
.focused($focusedField, equals: .phone)
}
}
}
不要占用系统保留的快捷键:Cmd+H(主屏幕)、Cmd+Tab(应用切换器)、Cmd+Space(聚焦搜索)、Globe 键组合。这些将不起作用并造成混淆。
当连接硬件键盘时,调整 UI。隐藏屏幕上的键盘快捷键栏。显示键盘优化的控件。使用 GCKeyboard 或跟踪键盘可见性来检测状态。
支持方向键用于导航列表、网格和集合。结合 Shift 键进行多选。这对于以生产力为中心的应用至关重要。
不要依赖用户记忆快捷键词汇。通过 Cmd 键长按叠加层、菜单标签和可见的焦点移动来公开命令,以便人们通过识别和重复来学习快捷键。
iPadOS 会自动在任何标准文本字段中将手写转换为文本。不要禁用随手写。对于自定义文本输入,采用 UIScribbleInteraction。测试随手写在所有文本输入点是否有效。
Apple Pencil 第二代及后续版本支持双击切换工具(例如,从画笔切换到橡皮擦)。如果你的应用有绘图工具,请实现 UIPencilInteraction 委托来处理双击。
对于绘图应用,响应来自铅笔触摸事件的 force(压力)和 altitudeAngle/azimuthAngle(倾斜)。使用这些来实现可变的线条宽度、不透明度或阴影。
支持悬停的 Apple Pencil(M2 iPad Pro 及后续机型)在铅笔接触屏幕之前提供位置数据。使用此功能实现预览效果、工具尺寸指示器和增强的精度。
// UIKit 通过 UIHoverGestureRecognizer 支持悬停
let hoverRecognizer = UIHoverGestureRecognizer(target: self, action: #selector(pencilHoverChanged(_:)))
hoverRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.pencil.rawValue)]
canvas.addGestureRecognizer(hoverRecognizer)
@objc func pencilHoverChanged(_ hover: UIHoverGestureRecognizer) {
let location = hover.location(in: canvas)
showBrushPreview(at: location)
}
对于笔记和批注,使用 PencilKit 中的 PKCanvasView。它提供了完整的绘图体验,包括工具选择器、撤销和墨迹识别,开箱即用。
import PencilKit
struct DrawingView: UIViewRepresentable {
@Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
canvasView.drawingPolicy = .anyInput
return canvasView
}
}
iPad 用户期望在应用之间拖放内容。支持将内容拖出(作为源)和将内容拖入(作为目标)。这是 iPad 的核心交互方式。
// 作为拖拽源
Text(item.title)
.draggable(item.title)
// 作为放置目标
DropTarget()
.dropDestination(for: String.self) { items, location in
handleDrop(items)
return true
}
用户可以拾取一个项目,然后点击其他项目将其添加到拖放中。通过提供多个 NSItemProvider 项目来支持多项目拖放。在拖放预览上显示徽章计数。
当拖拽到导航元素(文件夹、标签页、侧边栏项目)上时,短暂暂停以“弹簧打开”该目标。在导航容器上实现弹簧加载,以启用深层放置目标。
提供清晰的视觉状态:
通用控制允许用户在 iPad 和 Mac 之间拖放。如果你的应用支持使用标准 NSItemProvider 和 UTTypes 进行拖放,则通用控制会自动生效。
使用 DropDelegate 对放置行为进行细粒度控制:验证放置内容、在列表内重新排序以及处理放置位置。
struct ReorderDropDelegate: DropDelegate {
let item: Item
@Binding var items: [Item]
@Binding var draggedItem: Item?
func performDrop(info: DropInfo) -> Bool {
draggedItem = nil
return true
}
func dropEntered(info: DropInfo) {
guard let draggedItem,
let fromIndex = items.firstIndex(of: draggedItem),
let toIndex = items.firstIndex(of: item) else { return }
withAnimation {
items.move(fromOffsets: IndexSet(integer: fromIndex),
toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
}
}
}
当连接到外接显示器时,显示补充内容,而不是复制 iPad 屏幕。演示文稿、参考资料或扩展视图应显示在外接显示器上,而控件则保留在 iPad 上。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// 用于外接显示器的附加场景
WindowGroup(id: "presentation") {
PresentationView()
}
}
}
通过 SceneDelegate 中的 UIWindowScene 事件或监听 UIScene 会话通知(UIApplication.didConnectSceneSessionNotification / UIApplication.didDisconnectSceneSessionNotification)来观察外接显示器的生命周期。优雅地过渡 —— 如果外接显示器在演示过程中断开连接,将内容带回 iPad 屏幕而不丢失数据。
// SceneDelegate:检测场景(外接显示器窗口)何时连接或断开连接
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
configureExternalDisplay(for: windowScene)
}
func sceneDidDisconnect(_ scene: UIScene) {
restoreContentToiPad()
}
使用外接显示器的完整分辨率和宽高比。不要对你的内容进行信箱模式或邮筒模式处理。在 iOS 16+ 的多场景上下文中,UIScreen.main 已弃用 —— 通过 UIWindowScene.coordinateSpace.bounds 和 UIWindowScene.screen.scale 查询连接的显示器,或在 SwiftUI 中使用 @Environment(\.displayScale)。
影响: 关键
每个按钮、控件和交互元素都必须有有意义的无障碍标签。仅图标的工具栏项目和自定义视图必须使用 .accessibilityLabel()。
正确:
Button(action: compose) {
Image(systemName: "square.and.pencil")
}
.accessibilityLabel("撰写新消息")
错误:
Button(action: compose) {
Image(systemName: "square.and.pencil")
}
// VoiceOver 会读出 "square.and.pencil" —— 对用户毫无意义
使用语义文本样式(title、body、caption),以便文本随用户的偏好尺寸缩放。在 iPad 更大的画布上,切勿限制文本大小或禁用缩放。测试到五个无障碍尺寸级别。
Text("分区标题")
.font(.headline) // 自动随动态类型缩放
悬停状态(.hoverEffect)增强了指针输入,但不能作为交互性的唯一指示器。确保所有交互元素也能通过颜色、形状或标签为 VoiceOver 和仅使用键盘的用户区分开来。
启用完整键盘访问后,Tab 键必须按逻辑顺序在所有交互元素之间移动焦点。在分屏浏览和多窗口布局中,焦点不得逃逸到隐藏或被遮挡的窗口。使用 @FocusState 和 .focusable() 来控制键盘焦点图。
struct FormView: View {
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("姓名", text: $name)
.focused($focusedField, equals: .name)
TextField("邮箱", text: $email)
.focused($focusedField, equals: .email)
}
}
}
在分屏浏览中,每个应用都有自己的 VoiceOver 焦点上下文。你的应用不能假定它占据整个屏幕。确保 VoiceOver 即使在 1/3 或 1/2 分屏宽度下也能导航你的整个可见界面。不要将可操作内容隐藏在可见区域之外,除非同时将其从无障碍树中移除。
当用户在设置中启用粗体文本时,自定义渲染的文本必须适应。SwiftUI 文本样式会自动处理此问题。UIKit 代码必须检查 UIAccessibility.isBoldTextEnabled 或在 SwiftUI 中使用 @Environment(\.legibilityWeight)。
正确:
// SwiftUI —— 标准文本样式自动处理
Text("分区标题")
.font(.headline)
// SwiftUI —— 自定义渲染尊重 legibilityWeight
@Environment(\.legibilityWeight) var legibilityWeight
var body: some View {
Text("自定义标签")
.fontWeight(legibilityWeight == .bold ? .bold : .regular)
}
错误:
// 硬编码的权重忽略了粗体文本偏好
label.font = UIFont.systemFont(ofSize: 17, weight: .regular)
// 缺失:当 UIAccessibility.boldTextStatusDidChangeNotification 触发时重新查询字体
当用户在设置中启用增强对比度时,自定义颜色必须提供更高对比度的变体。在 SwiftUI 中使用 @Environment(\.colorSchemeContrast),或在 UIKit 中使用 UIAccessibility.isDarkerSystemColorsEnabled。
正确:
// SwiftUI
@Environment(\.colorSchemeContrast) var contrast
var separatorColor: Color {
contrast == .increased ? Color.primary : Color.secondary
}
// UIKit
let useHighContrast = UIAccessibility.isDarkerSystemColorsEnabled
let borderColor: UIColor = useHighContrast ? .label : .separator
错误:
// 静态颜色忽略了增强对比度设置
let borderColor = UIColor.separator // 始终为低对比度;忽略了用户偏好
使用此清单验证 iPad 就绪性:
horizontalSizeClass 的自适应布局UIAccessibility.isBoldTextEnabled)colorSchemeContrast 或 isDarkerSystemColorsEnabled 提供更高对比度的变体)将单列 iPhone UI 拉伸以填满 iPad 屏幕会浪费空间,显得懒惰,并提供糟糕的体验。始终为更大的画布重新设计。
切勿选择不支持多任务处理。用户期望每个应用都能在分屏浏览和侧拉中工作。要求全屏对 iPad 工作流程是不友好的。
许多 iPad 用户拥有妙控键盘或智能键盘。一个没有键盘快捷键的应用迫使他们不断伸手触摸屏幕。为所有频繁操作提供快捷键。
底部的标签栏在 iPad 上浪费垂直空间且看起来格格不入。在常规宽度下转换为侧边栏导航。SwiftUI 通过 .sidebarAdaptable 自动完成此操作。
在 iPad 上,弹出框应锚定到其源元素作为浮动面板。仅对沉浸式内容或真正需要全屏的流程使用全屏表单。避免一切都使用表单的 iPhone 模式。
缺少悬停效果会让应用在使用触控板时感觉像是坏了。用户无法分辨什么是可交互的。始终为自定义交互元素添加悬停反馈。
切勿基于特定 iPad 型号硬编码宽度、高度或位置。使用自动布局约束、SwiftUI 灵活框架和 GeometryReader 进行动态尺寸调整。
在 iPad 上,应用间拖放是核心工作流程。不支持它会使你的应用成为内容的死胡同。至少,支持拖入和拖出文本、图像和 URL。
占用 Cmd+H、Cmd+Tab、Cmd+Space 或 Globe 快捷键将不起作用,并会让期望系统行为的用户感到困惑。在分配之前检查苹果的保留快捷键列表。
大尺寸的 iPad 屏幕会诱惑设计师一次性展示所有内容。当内容超出可见区域时,仍应滚动。切勿为了避免滚动而截断内容。
每周安装次数
266
代码仓库
GitHub 星标数
283
首次出现
2026年2月1日
安全审计
安装于
codex226
opencode222
gemini-cli212
claude-code211
github-copilot198
cursor182
Comprehensive rules for building iPad-native apps following Apple's Human Interface Guidelines. iPad is not a big iPhone -- it demands adaptive layouts, multitasking support, pointer interactions, keyboard shortcuts, and inter-app drag and drop. These rules extend iOS patterns for the larger, more capable canvas.
iPad presents two horizontal size classes: regular (full screen, large splits) and compact (Slide Over, narrow splits). Design for both. Never hardcode dimensions.
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .regular {
TwoColumnLayout()
} else {
StackedLayout()
}
}
}
iPad layouts must be purpose-built. Stretching an iPhone layout across a 13" display wastes space and feels wrong. Use multi-column layouts, master-detail patterns, and increased information density in regular width.
Design for the full range: iPad Mini (8.3"), iPad (11"), iPad Air (11"/13"), and iPad Pro (11"/13"). Use flexible layouts that redistribute content rather than simply scaling.
In regular width, organize content into columns. Two-column is the most common (sidebar + detail). Three-column works for deep hierarchies (sidebar + list + detail). Avoid single-column full-width layouts on large screens.
struct ThreeColumnLayout: View {
var body: some View {
NavigationSplitView {
SidebarView()
} content: {
ContentListView()
} detail: {
DetailView()
}
}
}
iPad safe areas differ from iPhone. Older iPads have no home indicator. iPads in landscape have different insets than portrait. Always use safeAreaInset and never hardcode padding for notches or indicators.
iPad apps must work well in both portrait and landscape. Landscape is the dominant orientation for productivity. Portrait is common for reading. Adapt column counts and layout density to orientation.
Your app must function correctly at 1/3, 1/2, and 2/3 screen widths in Split View. At 1/3 width, your app receives compact horizontal size class. Content must remain usable at every split ratio.
Slide Over presents your app as a compact-width overlay on the right edge. It behaves like an iPhone-width app. Ensure all functionality remains accessible in this narrow mode.
Stage Manager allows freely resizable windows and multiple windows simultaneously. Your app must:
Resize fluidly to arbitrary dimensions
Support multiple scenes (windows) showing different content
Not assume any fixed size or aspect ratio
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } // Support multiple windows WindowGroup("Detail", for: Item.ID.self) { $itemId in DetailView(itemId: itemId) } } }
The app may launch directly into Split View or Stage Manager. Do not depend on full-screen dimensions during setup, onboarding, or any flow. Test your app at every possible size.
When the user resizes via multitasking, animate layout changes smoothly. Preserve scroll position, selection state, and user context across size transitions. Never reload content on resize.
Use UIScene / SwiftUI WindowGroup to let users open multiple instances of your app showing different content. Each scene is independent. Support NSUserActivity for state restoration.
In regular width, replace the iPhone tab bar with a sidebar. The sidebar provides more room for navigation items, supports sections, and feels native on iPad.
struct AppNavigation: View {
@State private var selection: NavigationItem? = .inbox
var body: some View {
NavigationSplitView {
List(selection: $selection) {
Section("Main") {
Label("Inbox", systemImage: "tray")
.tag(NavigationItem.inbox)
Label("Drafts", systemImage: "doc")
.tag(NavigationItem.drafts)
Label("Sent", systemImage: "paperplane")
.tag(NavigationItem.sent)
}
Section("Labels") {
// Dynamic sections
}
}
.navigationTitle("Mail")
} detail: {
DetailView(for: selection)
}
}
}
SwiftUI TabView with .sidebarAdaptable style automatically converts to a sidebar in regular width. Use this for seamless iPhone-to-iPad adaptation.
TabView {
Tab("Home", systemImage: "house") { HomeView() }
Tab("Search", systemImage: "magnifyingglass") { SearchView() }
Tab("Profile", systemImage: "person") { ProfileView() }
}
.tabViewStyle(.sidebarAdaptable)
Use NavigationSplitView with three columns when your information architecture has three levels: category > list > detail. Examples: mail (accounts > messages > message), file managers, settings.
On iPad, toolbars live at the top of the screen in the navigation bar area, not at the bottom like iPhone. Place contextual actions in .toolbar with appropriate placement.
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Compose", systemImage: "square.and.pencil") { }
}
ToolbarItemGroup(placement: .secondaryAction) {
Button("Archive", systemImage: "archivebox") { }
Button("Delete", systemImage: "trash") { }
}
}
When no item is selected in a list/sidebar, show a meaningful empty state in the detail area. Use a placeholder with icon and instruction text, not a blank screen.
Keep sidebar selection, search terms, and disclosure state visible and preserved across size changes and scene switches. In multi-column layouts, users should resume from structure on screen, not from memory.
All tappable elements should respond to pointer hover. The system provides automatic hover effects for standard controls. For custom views, use .hoverEffect().
Button("Action") { }
.hoverEffect(.highlight) // Subtle highlight on hover
// Custom hover effect
MyCustomView()
.hoverEffect(.lift) // Lifts and adds shadow
The pointer should snap to (be attracted toward) button bounds. Standard UIKit/SwiftUI buttons get this automatically. For custom hit targets, ensure the pointer region matches the tappable area using .contentShape().
Right-click (secondary click) should present context menus. Use .contextMenu which automatically supports both long-press (touch) and right-click (pointer).
Text(item.title)
.contextMenu {
Button("Copy", systemImage: "doc.on.doc") { }
Button("Share", systemImage: "square.and.arrow.up") { }
Divider()
Button("Delete", systemImage: "trash", role: .destructive) { }
}
Support two-finger scrolling with momentum. Pinch to zoom where appropriate. Respect scroll direction preferences. For custom scroll views, ensure trackpad gestures feel natural alongside touch gestures.
Change cursor appearance based on context. Text areas show I-beam. Links show pointer hand. Resize handles show resize cursors. Draggable items show grab cursor.
Pointer users expect click-and-drag for rearranging, selecting, and moving content. Combine with multi-select via Shift-click and Cmd-click.
Every primary action must have a keyboard shortcut. Standard shortcuts are mandatory:
| Shortcut | Action |
|---|---|
| Cmd+N | New item |
| Cmd+F | Find/Search |
| Cmd+S | Save |
| Cmd+Z | Undo |
| Cmd+Shift+Z | Redo |
| Cmd+C/V/X | Copy/Paste/Cut |
| Cmd+A | Select all |
| Cmd+P | |
| Cmd+W | Close window/tab |
| Cmd+, | Settings/Preferences |
| Delete | Delete selected item |
Button("New Document") { createDocument() }
.keyboardShortcut("n", modifiers: .command)
When the user holds the Cmd key, iPadOS shows a shortcut overlay. Register all shortcuts using .keyboardShortcut() so they appear in this overlay. Group related shortcuts logically.
Support Tab to move forward and Shift+Tab to move backward between form fields and focusable elements. Use .focusable() and @FocusState to manage keyboard focus order.
struct FormView: View {
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
TextField("Phone", text: $phone)
.focused($focusedField, equals: .phone)
}
}
}
Do not claim shortcuts reserved by the system: Cmd+H (Home), Cmd+Tab (App Switcher), Cmd+Space (Spotlight), Globe key combinations. These will not work and create confusion.
Adapt UI when a hardware keyboard is connected. Hide the on-screen keyboard shortcut bar. Show keyboard-optimized controls. Use GCKeyboard or track keyboard visibility to detect state.
Support arrow keys for navigating lists, grids, and collections. Combine with Shift for multi-selection. This is essential for productivity-focused apps.
Do not rely on users memorizing shortcut vocabularies. Expose commands through the Cmd-hold overlay, menu labels, and visible focus movement so people learn shortcuts by recognition and repetition.
iPadOS converts handwriting to text in any standard text field automatically. Do not disable Scribble. For custom text input, adopt UIScribbleInteraction. Test that Scribble works in all text entry points.
Apple Pencil 2 and later supports double-tap to switch tools (e.g., pen to eraser). If your app has drawing tools, implement the UIPencilInteraction delegate to handle double-tap.
For drawing apps, respond to force (pressure) and altitudeAngle/azimuthAngle (tilt) from pencil touch events. Use these for variable line width, opacity, or shading.
Apple Pencil with hover (M2 iPad Pro and later) provides position data before the pencil touches the screen. Use this for preview effects, tool size indicators, and enhanced precision.
// UIKit hover support via UIHoverGestureRecognizer
let hoverRecognizer = UIHoverGestureRecognizer(target: self, action: #selector(pencilHoverChanged(_:)))
hoverRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.pencil.rawValue)]
canvas.addGestureRecognizer(hoverRecognizer)
@objc func pencilHoverChanged(_ hover: UIHoverGestureRecognizer) {
let location = hover.location(in: canvas)
showBrushPreview(at: location)
}
For note-taking and annotation, use PKCanvasView from PencilKit. It provides a full drawing experience with tool picker, undo, and ink recognition out of the box.
import PencilKit
struct DrawingView: UIViewRepresentable {
@Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .black, width: 5)
canvasView.drawingPolicy = .anyInput
return canvasView
}
}
iPad users expect to drag content between apps. Support dragging content out (as a source) and dropping content in (as a destination). This is a core iPad interaction.
// As drag source
Text(item.title)
.draggable(item.title)
// As drop destination
DropTarget()
.dropDestination(for: String.self) { items, location in
handleDrop(items)
return true
}
Users can pick up one item, then tap additional items to add them to the drag. Support multi-item drag by providing multiple NSItemProvider items. Show a badge count on the drag preview.
When dragging over a navigation element (folder, tab, sidebar item), pause briefly to "spring open" that destination. Implement spring-loading on navigation containers to enable deep drop targets.
Provide clear visual states:
Universal Control lets users drag between iPad and Mac. If your app supports drag and drop with standard NSItemProvider and UTTypes, Universal Control works automatically.
Use DropDelegate for fine-grained control over drop behavior: validating drop content, reordering within lists, and handling drop position.
struct ReorderDropDelegate: DropDelegate {
let item: Item
@Binding var items: [Item]
@Binding var draggedItem: Item?
func performDrop(info: DropInfo) -> Bool {
draggedItem = nil
return true
}
func dropEntered(info: DropInfo) {
guard let draggedItem,
let fromIndex = items.firstIndex(of: draggedItem),
let toIndex = items.firstIndex(of: item) else { return }
withAnimation {
items.move(fromOffsets: IndexSet(integer: fromIndex),
toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
}
}
}
When connected to an external display, show complementary content rather than duplicating the iPad screen. Presentations, reference material, or expanded views belong on the external display while controls stay on iPad.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
// Additional scene for external display
WindowGroup(id: "presentation") {
PresentationView()
}
}
}
Observe external display lifecycle via UIWindowScene events in your SceneDelegate or by listening for UIScene session notifications (UIApplication.didConnectSceneSessionNotification / UIApplication.didDisconnectSceneSessionNotification). Transition gracefully — if the external display disconnects mid-presentation, bring content back to the iPad screen without data loss.
// SceneDelegate: detect when a scene (external display window) connects or disconnects
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
configureExternalDisplay(for: windowScene)
}
func sceneDidDisconnect(_ scene: UIScene) {
restoreContentToiPad()
}
Use the full resolution and aspect ratio of the external display. Do not letterbox or pillarbox your content. In iOS 16+ multi-scene contexts, UIScreen.main is deprecated — query the connected display via UIWindowScene.coordinateSpace.bounds and UIWindowScene.screen.scale, or use @Environment(\.displayScale) in SwiftUI.
Impact: CRITICAL
Every button, control, and interactive element must have a meaningful accessibility label. Icon-only toolbar items and custom views must use .accessibilityLabel().
Correct:
Button(action: compose) {
Image(systemName: "square.and.pencil")
}
.accessibilityLabel("Compose new message")
Incorrect:
Button(action: compose) {
Image(systemName: "square.and.pencil")
}
// VoiceOver reads "square.and.pencil" — meaningless to users
Use semantic text styles (title, body, caption) so text scales with the user's preferred size. In iPad's larger canvas, never clamp text size or disable scaling. Test up to the five accessibility size steps.
Text("Section Header")
.font(.headline) // Scales with Dynamic Type automatically
Hover states (.hoverEffect) enhance pointer input but must not be the sole indicator of interactivity. Ensure all interactive elements are also distinguishable via color, shape, or label for VoiceOver and keyboard-only users.
With Full Keyboard Access enabled, Tab must move focus through all interactive elements in logical order. In Split View and multi-window layouts, focus must not escape to a hidden or occluded window. Use @FocusState and .focusable() to control the keyboard focus graph.
struct FormView: View {
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
}
}
}
In Split View, each app has its own VoiceOver focus context. Your app must not assume it occupies the full screen. Ensure VoiceOver can navigate your entire visible interface even at 1/3 or 1/2 split width. Do not hide actionable content outside the visible region without also removing it from the accessibility tree.
When the user enables Bold Text in Settings, custom-rendered text must adapt. SwiftUI text styles handle this automatically. UIKit code must check UIAccessibility.isBoldTextEnabled or use @Environment(\.legibilityWeight) in SwiftUI.
Correct:
// SwiftUI — handled automatically for standard text styles
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)
}
Incorrect:
// Hardcoded weight ignores Bold Text preference
label.font = UIFont.systemFont(ofSize: 17, weight: .regular)
// Missing: re-query font when UIAccessibility.boldTextStatusDidChangeNotification fires
When the user enables Increase Contrast in Settings, custom colors must provide higher-contrast variants. Use @Environment(\.colorSchemeContrast) in SwiftUI or UIAccessibility.isDarkerSystemColorsEnabled in UIKit.
Correct:
// SwiftUI
@Environment(\.colorSchemeContrast) var contrast
var separatorColor: Color {
contrast == .increased ? Color.primary : Color.secondary
}
// UIKit
let useHighContrast = UIAccessibility.isDarkerSystemColorsEnabled
let borderColor: UIColor = useHighContrast ? .label : .separator
Incorrect:
// Static color ignores Increase Contrast setting
let borderColor = UIColor.separator // Always low-contrast; ignores user preference
Use this checklist to verify iPad-readiness:
horizontalSizeClassUIAccessibility.isBoldTextEnabled)colorSchemeContrast or isDarkerSystemColorsEnabled)Stretching a single-column iPhone UI to fill an iPad screen wastes space, looks lazy, and provides a poor experience. Always redesign for the larger canvas.
Never opt out of multitasking support. Users expect every app to work in Split View and Slide Over. Requiring full screen is hostile to iPad workflows.
Many iPad users have Magic Keyboard or Smart Keyboard. An app with no keyboard shortcuts forces them to reach for the screen constantly. Provide shortcuts for all frequent actions.
Tab bars at the bottom waste vertical space on iPad and look out of place. Convert to sidebar navigation in regular width. SwiftUI does this automatically with .sidebarAdaptable.
On iPad, popovers should anchor to their source element as floating panels. Only use full-screen sheets for immersive content or flows that genuinely need the full screen. Avoid the iPhone pattern of everything being a sheet.
Missing hover effects make the app feel broken when using a trackpad. Users cannot tell what is interactive. Always add hover feedback to custom interactive elements.
Never hardcode widths, heights, or positions based on a specific iPad model. Use Auto Layout constraints, SwiftUI flexible frames, and GeometryReader for dynamic sizing.
On iPad, drag and drop between apps is a core workflow. Not supporting it makes your app a dead end for content. At minimum, support dragging text, images, and URLs in and out.
Claiming Cmd+H, Cmd+Tab, Cmd+Space, or Globe shortcuts will not work and confuses users who expect system behavior. Check Apple's reserved shortcuts list before assigning.
Large iPad screens tempt designers to show everything at once. Content should still scroll when it exceeds the visible area. Never truncate content to avoid scrolling.
Weekly Installs
266
Repository
GitHub Stars
283
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex226
opencode222
gemini-cli212
claude-code211
github-copilot198
cursor182
前端设计技能指南:避免AI垃圾美学,打造独特生产级界面
36,100 周安装