swiftui-layout-components について
SwiftUI アプリ開発者向けのレイアウトとコンポーネントのスキルです。リスト、スクロール、フォーム、検索、オーバーレイを扱い、コンテナ選択やよくある誤りも解説します。
主な機能
- 01スタックとグリッドのレイアウトパターン
- 02リストとスクロールビューのパターン
- 03フォーム、コントロール、検索
- 04オーバーレイと表示方法の指針
- 05パフォーマンスと誤りの確認
活用シーン
- 01大規模なコレクションに遅延レイアウトを選択
- 02設定フォームや入力画面の構築
- 03リストへの検索とスクロール動作の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftui-layout-componentsインストール後、Claude Code で/swiftui-layout-componentsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/form.mdgrids.mdlist.mdscrollview.md詳細
SwiftUI のレイアウトとコンポーネント
iOS 26 以降を対象とする SwiftUI アプリのレイアウトとコンポーネントのパターンです。Swift 6.3 に対応しています。スタックとグリッドのレイアウト、リストのパターン、スクロールビュー、フォーム、コントロール、検索、オーバーレイについて説明します。特に記載がない限り、iOS 17 以降との後方互換性があります。
目次
レイアウトの基本
標準スタック
小規模で固定サイズのコンテンツには、VStack、HStack、ZStack を使用します。これらはすべての子ビューをすぐに描画します。
VStack(alignment: .leading) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
遅延スタック
大規模または動的なコレクションには、ScrollView 内で LazyVStack と LazyHStack を使用します。スクロールして表示範囲に入ったときに、必要に応じて子ビューを作成します。
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}
使い分け:
- 非遅延スタック: 小規模で固定されたコンテンツ(ヘッダー、ツールバー、フィールド数の少ないフォーム)
- 遅延スタック: 大規模またはサイズが不明なコレクション、フィード、チャットメッセージ
グリッドレイアウト
アイコンピッカー、メディアギャラリー、密度の高いビジュアル選択には LazyVGrid を使用します。デバイスサイズに応じて拡大縮小するレイアウトには .adaptive 列を、列数を固定するレイアウトには .flexible 列を使用します。
// Adaptive grid -- columns adjust to fit
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns) {
ForEach(items) { item in
ThumbnailView(item: item)
.aspectRatio(1, contentMode: .fit)
}
}
// Fixed 3-column grid
let columns = Array(repeating: GridItem(.flexible(minimum: 100), spacing: 4), count: 3)
LazyVGrid(columns: columns, spacing: 4) {
ForEach(items) { item in
ThumbnailView(item: item)
}
}
セルのサイズ設定には .aspectRatio を使用します。遅延コンテナ内に GeometryReader を配置しないでください。先行してサイズを測定するため、遅延読み込みの利点が失われます。寸法を読み取る必要がある場合は、.onGeometryChange(iOS 16 以降)を使用します。
グリッドのパターンと設計上の選択肢については、references/grids.md を参照してください。
リストのパターン
組み込みの行再利用、選択、アクセシビリティが重要なフィード形式のコンテンツや設定の行には、List を使用します。
List {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) { }
}
}
.listStyle(.insetGrouped)
主なパターン:
- フィードレイアウトには
.listStyle(.plain)、設定には.insetGroupedを使用します - テーマに合わせた背景には、
.scrollContentBackground(.hidden)とカスタム背景を組み合わせます - 余白と区切り線を調整するには、
.listRowInsets(...)と.listRowSeparator(.hidden)を使用します - 端へのスクロール: 上端/下端へのスクロール操作には、
ListとScrollPositionを.scrollPosition($scrollPosition)と組み合わせて使用します - 項目またはセクションへのジャンプ: 確実に ID を指定してジャンプするには、
.scrollTargetLayout()と安定したターゲットを使ったScrollViewと遅延スタックを使用します - プルして更新するフィードには
.refreshable { }を使用します - 行の端から端までタップ可能にするには、
.contentShape(Rectangle())を使用します - レイアウトのレビューや移行については、まずコンテナの選択と制約を示します。コードスニペットは短く保ち、スプリング、トランジション、タイミングの選択は
swiftui-animationに委ねます
iOS 26: 最新のスクロール端エフェクトには .scrollEdgeEffectStyle(.soft, for: .top) を適用します。
スクロールして先頭に戻るフィードリストなど、リストのパターン全般については、references/list.md を参照してください。
ScrollView
カスタムレイアウト、さまざまな種類のコンテンツの混在、または横スクロールが必要な場合は、ScrollView と遅延スタックを使用します。
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
ScrollPosition: 宣言的な双方向のスクロール位置の追跡と、プログラムによるスクロールを可能にします。
@State private var scrollPosition = ScrollPosition(edge: .bottom)
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
}
}
.scrollTargetLayout()
}
.scrollPosition($scrollPosition)
.onChange(of: messages.last?.id) {
withAnimation { scrollPosition.scrollTo(edge: .bottom) }
}
safeAreaInset(edge:) は、スクロールレイアウトに影響を与えずに、コンテンツ(入力バー、ツールバー)をキーボードの上に固定します。
iOS 26 での追加機能:
.scrollEdgeEffectStyle(.soft, for: .top)-- フェードする端エフェクト.backgroundExtensionEffect()-- セーフエリアの端で背景を反転/ぼかします(控えめに使用し、1 画面につき 1 つにします).safeAreaBar(edge:)-- スクロールエフェクトと連携するバーのビューを追加します
ScrollPosition、ページ単位の表示、ズームと切り抜きの競合、iOS 26 の端エフェクトのパターン全般については、references/scrollview.md を参照してください。
フォームとコントロール
Form
構造化された設定画面や入力画面には Form を使用します。関連するコントロールは Section ブロックにまとめます。
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $prefs.mentions)
Toggle("Follows", isOn: $prefs.follows)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
ForEach(Theme.allCases, id: \.self) { Text($0.title).tag($0) }
}
Slider(value: $fontScale, in: 0.5...1.5, step: 0.1)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
入力の多いフォームでキーボードのフォーカスを管理するには、@FocusState を使用します。単独で表示する場合やシート内で表示する場合に限り、NavigationStack でラップします。
コントロール
| コントロール | 用途 |
|---|---|
Toggle | 真偽値の設定 |
Picker | 選択肢からの選択。2〜4 個の場合は .segmented |
Slider | 値ラベルを表示する数値範囲 |
DatePicker | 日付/時刻の選択 |
TextField | テキスト入力。.keyboardType、.textInputAutocapitalization を使用 |
コントロールは @State、@Binding、または @AppStorage に直接バインドします。関連するコントロールは Form のセクションにまとめます。ロックされた設定や継承された設定を反映するには .disabled(...) を使用します。明確さにつながる場合は、トグル内で Label を使い、アイコンとテキストを組み合わせます。
選択肢が多い場合は .pickerStyle(.segmented) を避け、メニューまたはインラインスタイルを使用します。スライダーのラベルを隠さず、常に内容が分かるようにします。
フォームの全例については、references/form.md を参照してください。
検索可能なビュー
.searchable を使ってネイティブの検索 UI を追加します。複数のモードには .searchScopes を使い、非同期検索結果の入力をデバウンスするには .task(id:) を使います。
@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("Search")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}
検索欄が空の場合はプレースホルダーを表示します。過剰なデータ取得を避けるため、入力をデバウンスします。検索状態はビュー内に保持します。空文字列に対する検索は実行しないでください。
オーバーレイと表示
レイアウトに影響させず、一時的な UI(トースト、バナー)を表示するには .overlay(alignment:) を使います。
struct AppRootView: View {
@State private var toast: Toast?
var body: some View {
content
.overlay(alignment: .top) {
if let toast {
ToastView(toast: toast)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
Task {
try? await Task.sleep(for: .seconds(2))
withAnimation { self.toast = nil }
}
}
}
}
}
}
一時的な UI には、レイアウトスタックに埋め込むのではなく、オーバーレイを優先します。トランジションと短い自動非表示タイマーを使います。オーバーレイは明確な端(.top または .bottom)に配置します。明示的に必要な場合を除き、すべての操作を妨げるオーバーレイは避けます。オーバーレイを重ねすぎないでください。キューを使うか、現在のトーストを置き換えます。
モーダルのルーティング、シートのデテント、全画面表示の方針については、swiftui-navigation スキルに引き継ぎます。
よくある間違い
- 遅延コンテナ内に
GeometryReaderを配置すると遅延読み込みが機能しなくなります。寸法が必要な場合は.onGeometryChangeを使います。 - 配列のインデックスは不安定な
ForEachID になり、差分計算が正しく行われません。 - 同じ軸のスクロールビューを入れ子にすると、ジェスチャーが競合します。
- 複雑なカスタム行や展開する
Listの行は、ScrollView+LazyVStackに配置します。 - 選択肢が多い場合は
.segmentedではなく、メニューまたはインラインのピッカースタイルを使います。 - 特定の間隔を意図している場合を除き、プラットフォームに適応するデフォルト値を使うため、スタック/グリッドの
spacing:は省略します。 - スクロールに連動する表示には、並行する複数の真偽値や重複するドラッグジェスチャーではなく、正規化された進捗値を 1 つ使います。
- フレームごとのスクロールジオメトリはローカルに保ち、進捗計算に使うジオメトリを変更しないようにします。
レビューチェックリスト
- 大規模または動的なコレクションでは
LazyVStack/LazyHStackを使用している - すべての
ForEach項目で安定したIdentifiableID を使用している(配列インデックスは不可) - 遅延コンテナ内に
GeometryReaderを配置していない -
Listのスタイルが用途に合っている(フィードには.plain、設定には.insetGrouped) - 構造化された入力画面にカスタムスタックではなく
Formを使用している -
.searchableの入力を.task(id:)でデバウンスしている - データソースがプル更新に対応している箇所に
.refreshableを追加している - オーバーレイにトランジションと自動非表示タイマーを使用している
- タップ可能な行に
.contentShape(Rectangle())を指定している - フォームで
@FocusStateによりキーボードフォーカスを管理している - 特定の値が必要な場合を除き、スタック/グリッドの
spacing:を省略している - スクロールに連動する表示に正規化された進捗値を 1 つ使い、ジオメトリの更新範囲を狭いサブツリー内に保っている
- ズーム/切り抜き操作が競合する場合はスクロールを無効にし、離散的な表示効果で連続アニメーションを駆動していない
リファレンス
- グリッドのパターン:references/grids.md
- リストとセクションのパターン:references/list.md
- ScrollView と遅延スタック:references/scrollview.md
- フォームのパターン:references/form.md
- アーキテクチャと状態管理:
swiftui-patternsスキルを参照 - ナビゲーションのパターン:
swiftui-navigationスキルを参照