pinNest UI/UX ガイドライン
pinNest UI/UX ガイドライン
概要
本ドキュメントは pinNest の UI/UX 設計方針を定めます。 Apple の Human Interface Guidelines(HIG) をベースとし、iOS ネイティブの体験に沿った一貫性のある UI を提供することを目的とします。
参考: Apple Human Interface Guidelines
設計の基本原則
Apple HIG が掲げる 3 つの原則をプロジェクト全体で遵守します。
| 原則 | 説明 |
|---|---|
| Clarity(明確さ) | テキスト・アイコン・カラーを使って情報を正確に伝える。装飾より機能を優先する |
| Deference(従順さ) | コンテンツを主役にする。UI はコンテンツを引き立てる背景として機能する |
| Depth(奥行き) | 視覚的な階層・アニメーション・トランジションで空間的な理解を助ける |
カラー
システムカラーを優先する
独自カラーより Color のシステムカラーを優先します。ダークモード・アクセシビリティへの対応が自動的に得られます。
// Good
Text("タイトル").foregroundStyle(.primary)
Rectangle().fill(Color(.systemBackground))
Button("保存") { }.tint(.blue) // システムブルー
// Bad: ハードコードしたカラー値
Text("タイトル").foregroundStyle(Color(red: 0.1, green: 0.1, blue: 0.1))
カラーの役割定義
| 役割 | 使用するカラー |
|---|---|
| プライマリテキスト | .primary |
| セカンダリテキスト | .secondary |
| 背景 | Color(.systemBackground) / Color(.secondarySystemBackground) |
| アクセントカラー | .tint(アプリ全体で統一) |
| 危険操作 | .red |
| 成功・完了 | .green |
ダークモード対応
- ハードコードしたカラーを使用する場合は
Color(light:dark:)で両モードを定義する Assets.xcassetsの Color Set でライト / ダークを設定する
タイポグラフィ
Dynamic Type を必ず使用する
固定サイズの font(.system(size: 14)) ではなく、Dynamic Type スタイルを使用します。
これによりユーザーのフォントサイズ設定が反映されます。
// Good
Text("見出し").font(.headline)
Text("本文").font(.body)
Text("注釈").font(.caption)
// Bad
Text("見出し").font(.system(size: 17, weight: .semibold))
テキストスタイルの使い分け
| スタイル | 用途 |
|---|---|
.largeTitle |
画面タイトル(NavigationStack の大見出し) |
.title / .title2 / .title3 |
セクションタイトル |
.headline |
リストアイテムのメインラベル |
.body |
通常の本文テキスト |
.subheadline / .callout |
補足情報 |
.footnote / .caption |
メタ情報・タイムスタンプ |
スペーシング・レイアウト
余白は 8pt グリッドを基準にする
| サイズ | 用途 |
|---|---|
4pt |
最小マージン(アイコンとラベルの間など) |
8pt |
コンポーネント内の標準スペース |
16pt |
コンテンツの水平パディング(画面端からの余白) |
24pt |
セクション間のスペース |
32pt |
大きなセクション区切り |
// Good
VStack(spacing: 8) { ... }
.padding(.horizontal, 16)
// Bad: 中途半端な値
VStack(spacing: 11) { ... }
.padding(.horizontal, 13)
Safe Area を尊重する
コンテンツが Safe Area に重ならないようにします。カスタム背景など意図的にはみ出す場合のみ .ignoresSafeArea() を使用します。
コンポーネント
ネイティブコンポーネントを優先する
カスタム実装より標準コンポーネントを優先します。OS アップデートで自動的に改善されます。
| 用途 | 使用するコンポーネント |
|---|---|
| リスト表示 | List |
| 画面遷移 | NavigationStack / NavigationLink |
| タブ切り替え | TabView |
| モーダル | .sheet / .fullScreenCover |
| アラート・確認 | .alert / .confirmationDialog |
| アクション選択 | .contextMenu / Menu |
| 入力 | TextField / Toggle / Picker |
ボタン
- 主要アクションは
.buttonStyle(.borderedProminent)を使用する - 危険な操作(削除など)には
.tint(.red)を付与する - アイコンボタンには
Labelを使い、アクセシビリティラベルを持たせる
// 主要アクション
Button("保存") { store.send(.saveButtonTapped) }
.buttonStyle(.borderedProminent)
// 危険アクション
Button(role: .destructive) {
store.send(.deleteButtonTapped)
} label: {
Label("削除", systemImage: "trash")
}
// アイコンボタン
Button {
store.send(.addButtonTapped)
} label: {
Label("追加", systemImage: "plus")
.labelStyle(.iconOnly)
}
アイコン
- アイコンは SF Symbols を使用する(独自画像は不要な限り使わない)
Image(systemName:)で呼び出し、サイズ調整は.imageScale()または.font()で行う
// Good
Image(systemName: "pin.fill")
.imageScale(.medium)
// サイズをテキストに合わせる
Label("ピン留め", systemImage: "pin.fill")
.font(.body)
ナビゲーション
NavigationStack を使用する
NavigationStack+NavigationLinkで画面遷移を管理する- TCA の
@Presents/navigationDestinationと組み合わせて状態管理する
画面タイトル
- 各画面には
.navigationTitle()を必ず設定する - 一覧画面は
.navigationBarTitleDisplayMode(.large)、詳細・編集画面は.navigationBarTitleDisplayMode(.inline)を使用する
NavigationStack {
PinListView(store: store)
.navigationTitle("ピン一覧")
.navigationBarTitleDisplayMode(.large)
}
フィードバック
ユーザー操作に対して適切なフィードバックを返します。
| 状況 | フィードバック手段 |
|---|---|
| ロード中 | ProgressView() |
| 操作の成功 | .sensoryFeedback(.success, trigger:) |
| エラー | .alert でメッセージ表示 + .sensoryFeedback(.error, trigger:) |
| 削除・完了 | .sensoryFeedback(.impact, trigger:) |
// ハプティクスフィードバック
List { ... }
.sensoryFeedback(.success, trigger: store.isSaved)
アクセシビリティ
HIG はアクセシビリティを必須要件として位置づけています。
ラベルの付与
すべてのインタラクティブ要素にはアクセシビリティラベルを設定します。
// アイコンボタンには必ずラベルを付ける
Button {
store.send(.addButtonTapped)
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("ピンを追加")
コントラスト比
- テキストとその背景のコントラスト比は 4.5:1 以上(WCAG AA 準拠)を確保する
- システムカラーを使用していれば自動的に満たされる
タップ領域
タップ可能な要素の最小サイズは 44×44pt を確保します。
Button { ... } label: {
Image(systemName: "ellipsis")
}
.frame(minWidth: 44, minHeight: 44)
VoiceOver 対応
- 装飾的な画像には
.accessibilityHidden(true)を付与する - 複数要素をグループ化する場合は
.accessibilityElement(children: .combine)を使用する
アニメーション
- アニメーションは
.animation(.default, value:)を使用し、変化のトリガーを明示する - 過度なアニメーションはユーザーの集中を妨げるため避ける
- Reduce Motion 設定を尊重する
// Good: トリガーを明示した控えめなアニメーション
List { ... }
.animation(.default, value: store.items)
// Reduce Motion に配慮
@Environment(\.accessibilityReduceMotion) var reduceMotion
var animation: Animation {
reduceMotion ? .none : .spring(duration: 0.3)
}