ios-design-guidelines について
AppleのHuman Interface Guidelinesに基づく、iPhoneアプリの設計・レビュー・リファクタリング向けスキルです。レイアウト、ナビゲーション、文字、色、アクセシビリティを扱います。
主な機能
- 01レイアウトとセーフエリアの指針
- 02ナビゲーション構成の推奨事項
- 03Dynamic Typeと文字組みのルール
- 04ダークモードとコントラストの指針
- 05VoiceOverとアクセシビリティの指針
活用シーン
- 01iPhoneアプリの画面設計
- 02タップ領域とセーフエリアの確認
- 03画面サイズに応じたナビゲーション調整
- 04Dynamic Typeとアクセシビリティ対応のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
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 向け iOS デザインガイドライン
Apple の Human Interface Guidelines に基づく包括的なルールです。iPhone アプリのインターフェースを構築、レビュー、リファクタリングする際に適用してください。
1. レイアウトとセーフエリア
影響度: 重大
ルール 1.1:最小 44pt のタッチターゲット
すべてのインタラクティブ要素は、最小 44x44 ポイントのタップ領域を確保する必要があります。これにはボタン、リンク、トグル、カスタムコントロールが含まれます。
正しい例:
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:セーフエリアを尊重する
ステータスバー、Dynamic Island、ホームインジケータの下に、インタラクティブなコンテンツや重要なコンテンツを配置しないでください。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 グリッドに揃える
間隔、パディング、要素のサイズは、8 ポイントの倍数(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 を使用する
階層をたどるコンテンツには、非推奨の NavigationView ではなく NavigationStack を使用します。プログラムからのナビゲーションには 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. タイポグラフィと Dynamic Type
影響度: 高
ルール 3.1:組み込みのテキストスタイルを使用する
サイズをハードコードせず、常にセマンティックなテキストスタイルを使用します。これらのスタイルは Dynamic Type に合わせて自動的に拡大縮小します。
正しい例:
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:アクセシビリティサイズを含む Dynamic Type に対応する
Dynamic Type では、アクセシビリティサイズを最大にすると、テキストが約 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. 色とダークモード
影響度: HIGH
ルール 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:カスタムカラーにはライトとダークのバリエーションを用意する
アセットカタログでカスタムカラーを定義し、「Any Appearance」と「Dark Appearance」の両方のバリエーションを用意してください。
// 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:背景の階層
奥行きを出すために、3 段階の背景階層を使用してください。
systemBackground— メインのサーフェスsecondarySystemBackground— グループ化されたコンテンツ、カードtertiarySystemBackground— グループ化されたコンテンツ内の要素
ルール 4.7:インタラクティブ要素には 1 つのアクセントカラーを使用する
すべてのインタラクティブ要素(ボタン、リンク、トグル)に、単一のティントカラー/アクセントカラーを選んで使用してください。これにより、一貫性があり、習得しやすい視覚言語になります。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.tint(.indigo) // All interactive elements use indigo
}
}
}
5. アクセシビリティ
影響度: CRITICAL
ルール 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:スイッチコントロールとフルキーボードアクセスに対応する
すべての操作がスイッチコントロール(外部スイッチ)とフルキーボードアクセス(Bluetooth キーボード)で機能することを確認してください。ナビゲーション順序とフォーカスの動作をテストしてください。
6. ジェスチャと入力
影響度: 高
ルール 6.1:標準的なジェスチャを使用する
標準的な iOS のジェスチャ用語を使用します。タップ、長押し、スワイプ、ピンチ、回転です。ユーザーはすでにこれらを理解しています。
| ジェスチャ | 標準的な用途 |
|---|---|
| タップ | 主要なアクション、選択 |
| 長押し | コンテキストメニュー、プレビュー |
| 水平方向のスワイプ | 削除、アーカイブ、戻る |
| 垂直方向のスワイプ | スクロール、シートを閉じる |
| ピンチ | 拡大/縮小 |
| 2 本指での回転 | コンテンツの回転 |
ルール 6.2:システムジェスチャを上書きしない
以下のジェスチャはシステム用に予約されているため、横取りしてはいけません。
- 左端からのスワイプ(戻るナビゲーション)
- 左上端から下へのスワイプ(通知センター)
- 右上端から下へのスワイプ(コントロールセンター)
- 画面下端から上へのスワイプ(ホーム/アプリスイッチャー)
ルール 6.3:カスタムジェスチャは見つけやすくする
カスタムジェスチャを追加する場合は、視覚的なヒント(グラバーなど)を用意し、そのアクションを目に見えるボタンまたはメニュー項目からも実行できるようにします。
ルール 6.4:すべての入力方法に対応する
まずタッチ操作を前提に設計しますが、次にも対応します。
- ハードウェアキーボード(iPad 用キーボードアクセサリ、Bluetooth キーボード)
- 支援機器(スイッチコントロール、ヘッドトラッキング)
- ポインタ入力(AssistiveTouch)
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 Tracking Transparency
アプリやウェブサイトをまたいでユーザーを追跡する場合は、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 を使う
ユーザーが Siri、Spotlight、「ショートカット」アプリから主要な操作を実行できるよう、App Shortcuts を定義します。
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:ライブアクティビティ
配送状況の追跡、スポーツのスコア、ワークアウトなど、リアルタイムで一定時間だけ表示するイベントには、ライブアクティビティと Dynamic Island を使用します。
ルール 10.6:中断に適切に対応する
次の要因で中断された場合は、状態を保存して適切に一時停止します。
- 電話の着信
- Siri の起動
- 通知
- アプリスイッチャー
- 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 個) | .tabItem を使う TabView | 下部のタブバー、SF Symbols |
| 階層的な詳細表示 | NavigationStack | ルートでは大きなタイトル、子画面ではインライン |
| 独立したタスク | .sheet | スワイプで閉じる、キャンセル/完了ボタン |
| 重要な判断 | .alert | 2 つのボタンを推奨、最大 3 つ |
| 二次的なアクション | .contextMenu | 長押しで表示。ほかの場所からもアクセスできるようにする必要があります |
| スクロール可能なコンテンツ | .insetGrouped を使う List | 行の最小サイズは 44pt、スワイプアクション |
| テキスト入力 | TextField / TextEditor | ラベルを上に、検証メッセージを下に表示 |
| 選択(選択肢が少ない場合) | Picker | 2~5 項目ならセグメント型、多数ならホイール型 |
| 選択(オン/オフ) | Toggle | リストの行の右側に配置 |
| 検索 | .searchable | 候補、最近の検索 |
| 進捗(既知) | ProgressView(value:total:) | パーセンテージまたは残り時間を表示 |
| 進捗(未知) | ProgressView() | インラインで表示し、画面全体をブロックしない |
| 位置情報の一回限りの取得 | LocationButton | 継続的な許可は不要 |
| コンテンツの共有 | ShareLink | システムの共有シート |
| 触覚フィードバック | UIImpactFeedbackGenerator | .light、.medium、.heavy |
| 破壊的なアクション | Button(role: .destructive) | 赤色の色合い、アラートで確認 |
評価チェックリスト
このチェックリストを使って、iPhone アプリが HIG に準拠しているか確認します。
レイアウトとセーフエリア
- すべてのタッチ対象が少なくとも 44x44pt である
- ステータスバー、Dynamic Island、ホームインジケーターの下にコンテンツが隠れていない
- 主なアクションが画面の下半分(親指が届く範囲)にある
- 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 プロンプトを表示し、拒否された場合はその選択を尊重する
システム連携
- ウィジェットに、ひと目で確認できる最新情報が表示される
- アプリのコンテンツがSpotlightにインデックスされる
- 共有可能なコンテンツで共有シートを利用できる
- アプリが中断(通話、バックグラウンド、Siri)に適切に対応する
アンチパターン
以下は、iOSヒューマンインターフェイスガイドラインに違反するよくある間違いです。次のことは絶対に避けてください。
-
ハンバーガーメニュー — タブバーを使用してください。ハンバーガーメニューはナビゲーションを隠し、機能の見つけやすさを最大50%低下させます。
-
スワイプで戻る操作を妨げるカスタム戻るボタン — 戻るボタンを置き換える場合は、
NavigationStackを使って左端からのスワイプジェスチャーが引き続き機能するようにしてください。 -
画面全体を塞ぐスピナー — スケルトンビューやインラインの進行状況インジケーターを使用してください。画面全体を塞ぐスピナーがあると、アプリがフリーズしたように感じられます。
-
ロゴ入りのスプラッシュ画面 — 起動画面はアプリの最初の画面と同じ内容にする必要があります。ブランド表示による遅延は不自然に感じられます。
-
起動時にすべての権限を求める — 初回起動時にカメラ、位置情報、通知、連絡先へのアクセスを求めると、そのほとんどが拒否されます。
-
フォントサイズのハードコード — テキストスタイルを使用してください。ハードコードされたサイズはDynamic Typeやアクセシビリティ設定を無視し、何百万人ものユーザーにとってアプリが使いにくくなります。
-
状態を色だけで示す — 有効/無効を赤/緑だけで示すと、色覚障害のあるユーザーが利用できません。必ずアイコンやテキストも併用してください。
-
重要でない情報にアラートを使う — アラートは操作の流れを中断し、閉じる操作を必要とします。ヒントや重要でない情報には、バナー、トースト、インラインメッセージを使用してください。
-
画面遷移時にタブバーを隠す — タブ内のナビゲーション中は、タブバーを常に表示してください。タブバーを隠すと、ユーザーは現在の位置を把握しにくくなります。
-
セーフエリアを無視する — コンテンツビューで
.ignoresSafeArea()を使うと、ノッチ、Dynamic Island、ホームインジケーターの下にテキストやボタンが隠れてしまいます。 -
閉じられないモーダル — すべてのモーダルに、明確な閉じ方(閉じるボタン、キャンセル、下方向へのスワイプ)を用意してください。モーダルにユーザーを閉じ込めるのは不親切です。
-
代替手段のないカスタムジェスチャー — 3本指のスワイプによる取り消し操作は、多くの人にとって使いにくいものです。目に見えるボタンやメニュー項目も用意してください。
-
小さすぎるタッチ領域 — 44pt未満のボタンやリンクは、特にリストやツールバーで誤操作を招きます。
-
モーダルの多重表示 — シートの上にシートを重ねて表示すると、ナビゲーションが分かりにくくなります。代わりに、1つのモーダル内でナビゲーションしてください。
-
ダークモードを後回しにする — 色をハードコードすると、アプリはダークモードまたはライトモードのどちらかで正常に表示されなくなります。常にセマンティックカラーを使用してください。