mcpField
S
swiftui-layout-components

swiftui-layout-components

github.com/dpearson2699デザイン・UI1.2k4.3k 回インストール13 週間前に更新
リポジトリ

swiftui-layout-components について

SwiftUI アプリ開発者向けのレイアウトとコンポーネントのスキルです。リスト、スクロール、フォーム、検索、オーバーレイを扱い、コンテナ選択やよくある誤りも解説します。

主な機能

  1. 01スタックとグリッドのレイアウトパターン
  2. 02リストとスクロールビューのパターン
  3. 03フォーム、コントロール、検索
  4. 04オーバーレイと表示方法の指針
  5. 05パフォーマンスと誤りの確認

活用シーン

  1. 01大規模なコレクションに遅延レイアウトを選択
  2. 02設定フォームや入力画面の構築
  3. 03リストへの検索とスクロール動作の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-layout-components

インストール後、Claude Code で/swiftui-layout-componentsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
form.md
grids.md
list.md
scrollview.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 スキルに引き継ぎます。

よくある間違い

  1. 遅延コンテナ内に GeometryReader を配置すると遅延読み込みが機能しなくなります。寸法が必要な場合は .onGeometryChange を使います。
  2. 配列のインデックスは不安定な ForEach ID になり、差分計算が正しく行われません。
  3. 同じ軸のスクロールビューを入れ子にすると、ジェスチャーが競合します。
  4. 複雑なカスタム行や展開する List の行は、ScrollView + LazyVStack に配置します。
  5. 選択肢が多い場合は .segmented ではなく、メニューまたはインラインのピッカースタイルを使います。
  6. 特定の間隔を意図している場合を除き、プラットフォームに適応するデフォルト値を使うため、スタック/グリッドの spacing: は省略します。
  7. スクロールに連動する表示には、並行する複数の真偽値や重複するドラッグジェスチャーではなく、正規化された進捗値を 1 つ使います。
  8. フレームごとのスクロールジオメトリはローカルに保ち、進捗計算に使うジオメトリを変更しないようにします。

レビューチェックリスト

  • 大規模または動的なコレクションでは LazyVStack/LazyHStack を使用している
  • すべての ForEach 項目で安定した Identifiable ID を使用している(配列インデックスは不可)
  • 遅延コンテナ内に GeometryReader を配置していない
  • List のスタイルが用途に合っている(フィードには .plain、設定には .insetGrouped)
  • 構造化された入力画面にカスタムスタックではなく Form を使用している
  • .searchable の入力を .task(id:) でデバウンスしている
  • データソースがプル更新に対応している箇所に .refreshable を追加している
  • オーバーレイにトランジションと自動非表示タイマーを使用している
  • タップ可能な行に .contentShape(Rectangle()) を指定している
  • フォームで @FocusState によりキーボードフォーカスを管理している
  • 特定の値が必要な場合を除き、スタック/グリッドの spacing: を省略している
  • スクロールに連動する表示に正規化された進捗値を 1 つ使い、ジオメトリの更新範囲を狭いサブツリー内に保っている
  • ズーム/切り抜き操作が競合する場合はスクロールを無効にし、離散的な表示効果で連続アニメーションを駆動していない

リファレンス

コメント