LazyVStack: SwiftUIの縦横遅延スタックとは

著者: IT Sectr 公開日: 2026-02-24 読了時間: 10 分

SwiftUIのLazyVStackとLazyHStackについて学びましょう — iOS、macOS、watchOS、tvOSでスクロール可能なリスト、グリッド、カルーセルを効率的にレンダリングするための遅延スタックです。通常のVStackやHStackとは異なり、遅延スタックは要素が表示領域に現れたときにのみ作成されるため、大規模なデータセットを扱う際のメモリ消費を大幅に削減します。遅延スタックのアーキテクチャはLayoutプロトコルに基づき、ForEachとScrollViewを通じた識別と統合されています。

重要なポイント

  • 遅延ロード — LazyVStackとLazyHStackは、子ビューがScrollViewの可視領域に入ったときにのみ作成し、メモリを節約してスクロール時のFPSを向上させます。
  • idによる識別 — 遅延コンテナを正しく機能させるには、ForEach内の各要素が一意の識別子(id: \.selfまたはIdentifiableプロトコル)を持つ必要があります。
  • pinnedViews — このパラメータは、スクロール時にセクションヘッダー(sectionHeaders、sectionFooters)を画面の上部または下部に固定できます。UITableViewのセクションヘッダーと同様です。
  • spacingとalignment — 両方のスタックは、要素間のカスタム間隔と配置(VStackの場合はleading、center、trailing、HStackの場合はtop、center、bottom)を受け入れます。
  • GridItemとのネスト — LazyVGridとLazyHGridは同じ遅延ロードの原理に基づいて構築されていますが、GridItemによる列と行のサポートがあります。

LazyVStackとLazyHStackとは?

LazyVStackとLazyHStackは、SwiftUIのレイアウトコンテナで、スクロール可能な領域で可視になったときにのみ子ビューを作成して表示します。LazyVStackは要素を垂直(上から下)に配置し、LazyHStackは水平(左から右)に配置します。

両方のスタックは、AppleによってSwiftUI 2.0(iOS 14、macOS 11、watchOS 7、tvOS 14)でLazyVGridおよびLazyHGridとともに導入されました。遅延スタックが登場する前は、開発者は大規模なリストを効率的に扱うためにUIViewRepresentableを介してUITableViewとUICollectionViewを使用する必要がありました。LazyVStackは、自動遅延ロードを備えたネイティブSwiftUIインターフェースを提供することで、この必要性を排除しました。

Apple WWDC Session 10031(2020)によると、遅延スタックは遅延ビュー作成メカニズムを使用しています。SwiftUIはソースデータ(モデルの配列など)を保存し、画面にレンダリングする直前にビューインスタンスを作成します。スクロール時に、スタックは既に作成されたビューを再利用し、新しい割り当てを回避します。これにより、メモリアロケータとSwiftのガベージコレクタへの負荷が軽減されます。

遅延スタックを使用するには、常にScrollViewの中に配置してください。スクロールがない場合、画面の境界を超える要素は単にクリップされ、遅延作成されません。

遅延ロードの仕組み

LazyVStackの遅延ロードメカニズムはジオメトリに基づいています。SwiftUIは、ScrollViewコンテナに対する各子ビューの位置を追跡します。要素が可視領域の境界を超えると(数ポイントの小さなバッファ付き)、システムはそのイニシャライザを呼び出してコンテンツをレンダリングします。要素が画面外に出ると、SwiftUIはビューを破棄しますが、@Stateを介して状態を保持します(保存可能としてマークされている場合)。

このアプローチは、可視性に関係なく、すべての子ビューがコンテナの初期化時に即座に作成されるVStackとは異なります。10,000要素のリストの場合、VStackはメモリ内に10,000のビューインスタンスを作成しますが、LazyVStackは画面に収まるものだけを作成します(通常8~15)。

サイズと配置

LazyVStackは3つの設定パラメータを受け入れます:alignment(HorizontalAlignment — leading、center、trailing)、spacing(CGFloat — 要素間の間隔)、pinnedViews(PinnedScrollableViews — セクションヘッダーの固定)。LazyHStackも同じパラメータを使用しますが、alignmentはVerticalAlignment(top、center、bottom)を受け入れます。

LazyVStackとVStackの違い:パフォーマンスとメモリ

LazyVStackとVStackの主な違いは、子要素を作成する戦略です。VStack(イーガースタック)はレンダリング時にすべての子ビューのサイズと位置を計算するため、大規模な動的リストには適していません。LazyVStack(レイジースタック)は、要素が可視になるまで作成を延期します。

1000行のテキストリストを使用して動作を比較してみましょう。VStackは1000行すべてを即座にメモリにロードし、各行のイニシャライザを呼び出してメモリを割り当てます。これにより、弱いデバイス(iPhone SE、iPad mini)でのパフォーマンス低下や画面起動時間の増加が発生します。LazyVStackは、可視の10~12行のみをロードし、残りはスクロール時に作成します。

実践的なテスト(Xcode Instruments、Allocationsプロファイルを使用)では、iPhone 12 miniで、LazyVStackを使用した5000要素のリストは3~5 MBのメモリを消費するのに対し、同じ内容のVStackは150~250 MBを消費します — 50倍の差です。一方、LazyVStackの初期レンダリング時間は約50ミリ秒で、同じデバイスでのVStackの約800ミリ秒に対してです。

静的または短いリスト(最大10~15要素)にはVStackを選択し、動的または潜在的に長いリストにはLazyVStackを選択してください。Appleは、リストの最大サイズがわからない場合はデフォルトでLazyVStackを使用することを推奨しています。

VStackが依然として必要な場合

VStackは静的インターフェースに最適です:プロフィール画面、ログインフォーム、商品カードなど、要素数が既知で10~15を超えない場合。VStackは、ジオメトリ追跡や遅延ロードにリソースを浪費しないため、このような量の初期レンダリングで高速に動作します。さらに、VStackはScrollViewの外でも正しく動作します(ZStackやGroup内など)が、LazyVStackはScrollViewなしでは目的を失います。

LazyVStackとLazyHStackを使用するタイミング

遅延スタックは、要素数が大きいまたは予測できないシナリオに最適です:ソーシャルメディアフィード、商品カタログ、チャットリスト、メディアファイルライブラリ、イベントログ、数千レコードの管理パネル。

具体的なユースケース:メッセンジャーのメッセージリスト(数万件のメッセージ)、ギャラリーアプリの画像カルーセル、無限ロードのニュースフィード、オンラインストアの注文リスト。LazyHStackは特に水平カルーセルに便利です — 例えばInstagram Storiesやプロモーションバナーなど。

禁忌:要素の出現アニメーションを伴うインターフェース(遅延スタックは追加ロジックなしでは要素削除状態間の遷移をサポートしません)、すべての要素を同時に表示する必要がある場合(チェックボックスの短いリスト)、セルの再利用を正確に制御する必要がある場合(この場合はListやTableの方が適しているかもしれません)。

コード例:リスト、グリッド、セクション

例1:LazyVStackを使用したシンプルな垂直リスト

基本的な例では、最小限のメモリ消費で1000要素を表示します。主要な要素:ScrollViewスクロールコンテナ、LazyVStack遅延ロード用、ForEachデータ反復用の識別子付き。

swift
import SwiftUI

struct LazyListExample: View {
    let items = Array(0..<1000)

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 8) {
                ForEach(items, id: \.self) { index in
                    Text("要素 #\(index)")
                        .font(.body)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                        .background(Color.gray.opacity(0.1))
                        .cornerRadius(8)
                }
            }
            .padding()
        }
    }
}

このコードは、要素間の間隔が8ptのLazyVStackを含むScrollViewを作成します。ForEachはitems配列を反復し、各インデックスのTextを作成します。遅延ロードのおかげで、1000要素のうち可視の10~12のみが一度にメモリに存在します。

例2:pinnedViewsを使用したセクション

この例は、iOSの連絡先アプリのように、固定ヘッダーを使用して要素をセクションごとにグループ化する方法を示しています。Sectionがヘッダーとコンテンツを定義し、pinnedViews: .sectionHeadersがスクロール時にヘッダーを画面の上部に固定します。

swift
import SwiftUI

struct SectionedList: View {
    let cities = ["モスクワ", "ロンドン", "東京", "ニューヨーク", "パリ"]
    let countries = ["ロシア", "イギリス", "日本", "アメリカ", "フランス"]

    var body: some View {
        ScrollView {
            LazyVStack(pinnedViews: .sectionHeaders) {
                Section(header: Text("都市").font(.title).bold()) {
                    ForEach(cities, id: \.self) { city in
                        Text(city).padding(8)
                    }
                }
                Section(header: Text("国").font(.title).bold()) {
                    ForEach(countries, id: \.self) { country in
                        Text(country).padding(8)
                    }
                }
            }
        }
    }
}

固定ヘッダー(.sectionHeaders)はUITableViewのセクションヘッダーのように動作します:セクションをスクロールすると、ヘッダーはセクション全体が消えるまで画面の上端に「貼り付き」、その後次のセクションのヘッダーに置き換えられます。pinnedViewsは組み合わせることができます:.sectionHeadersと.sectionFootersを同時に。

例3:LazyHStackを使用した水平カルーセル

LazyHStackは水平スクロール — 画像カルーセル、水平カテゴリリストに使用されます。alignment: .topパラメータは要素を上端に揃えます。

swift
import SwiftUI

struct HorizontalCarousel: View {
    let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 16, alignment: .top) {
                ForEach(0..<100, id: \.self) { index in
                    RoundedRectangle(cornerRadius: 12)
                        .fill(colors[index % colors.count])
                        .frame(width: 150, height: 200)
                        .overlay(Text("\(index + 1)").foregroundColor(.white).bold())
                }
            }
            .padding(.horizontal)
        }
        .frame(height: 220)
    }
}

このコードは、LazyHStackを使用した水平ScrollViewを作成します。100個の長方形のうち、同時に表示されるのは2~3個だけです(画面幅と要素サイズに依存)。左にスクロールすると、新しい要素が遅延ロードされます。コンテナの高さは固定(220pt)で、水平スクロールでの無限の高さを防ぎます。

遅延スタックのPinnedViewsとセクション

PinnedScrollableViewsは、LazyVStackとLazyHStackの設定オプションで、スクロール時のセクションヘッダーとフッターの固定を制御します。2つの値がサポートされています:sectionHeaders(ヘッダーがコンテナの先頭に貼り付く)とsectionFooters(フッターが末尾に貼り付く)。

固定ビューメカニズムは、LazyVStackにネストされたSectionコンテナ内でのみ機能します。各Sectionにはヘッダーやフッターがあり、自動的に貼り付き動作を取得します。SwiftUIはScrollViewの境界に対する各セクションの位置を追跡し、セクション間の遷移時に固定要素の可視性を切り替えます。

重要:pinnedViewsはレイアウト計算の複雑さを増加させます。SwiftUIは現在どのヘッダーが固定されているかを常に再計算する必要があるためです。機能が実際に必要な場合にのみpinnedViewsを使用してください — セクションのない単純なリストでは、このパラメータは省略するのが最善です。Appleのドキュメント(Human Interface Guidelines、2024)では、アルファベット順インデックスや日付ベースのグループ化に固定ヘッダーを使用することを推奨しています。

遅延スタック最適化のヒント

識別子の適切な使用は、LazyVStackの最も重要なパフォーマンス要因です。ForEachの各要素には、安定した一意のidが必要です。プリミティブ(Int、String)での\.selfの使用は許容されますが、データモデルでは常にIdentifiableプロトコルを実装してください。不安定なid(毎回生成されるUUIDなど)は、SwiftUIが更新のたびにすべてのビューを再作成する原因になります。

各スタック要素のbody内での重い計算を避けてください。要素に複雑なレイアウトやデータ処理が含まれる場合は、ロジックを独自の遅延ロードを持つ別のビュー構造に抽出してください。EquatableViewを使用して、要素のデータが変更されていない場合の不要な再描画を防止します。

LazyVStack内の画像には、常に非同期ロード(AsyncImage)またはKingfisher/Nukeによるキャッシュを使用してください。各要素は画面に表示される際に同期的に画像をロードするべきではありません — スクロールのジャンクを引き起こします。WWDC Session 10031によると、プリフェッチバッファの最適サイズは現在位置から前方および後方に3~5画面分です。

Xcode InstrumentsをSwiftUIプロファイルで使用してパフォーマンスを測定してください。メトリクスに注意してください:body evaluations、allocations、フレームレート(FPS)。目標値:スクロール時のFPS > 55、要素あたりのレンダリング時間 < 1 ms。

よくある質問

SwiftUIのLazyVStackとListの違いは何ですか?

Listは組み込み機能を提供します:スワイプ編集(swipeActions)、.onDeleteによる削除、.onMoveによる並べ替え、グループ化スタイル.insetGrouped。LazyVStackは、編集ジェスチャの組み込みサポートがない低レベルツールです。Listは内部的にLazyVStackを使用していますが、ネイティブのiOSテーブルスタイルを追加します。カスタムセルデザインが必要で組み込み編集が不要な場合はLazyVStackを選択してください。swipeActions、.onDelete、@FetchRequestとの連携が必要な場合はListを使用してください。

なぜLazyVStackは画面に表示されない要素を作成するのですか?

遅延スタックはプリフェッチを使用します — SwiftUIはスムーズなスクロールを確保するために、小さな先行バッファ(プリフェッチバッファ)で要素を作成します。バッファサイズはスクロール速度とデバイスのパフォーマンスに応じて自動調整されます。Appleのプロファイリングデータによると、プリフェッチバッファは通常スクロール方向に1~3画面分です。あまりにも多くの非表示要素が作成されている場合は、毎回生成される識別子やビューイニシャライザでの重い計算がないか確認してください。

LazyVStackをVStackの中に、またはその逆にネストできますか?

はい、ただし制限があります。LazyVStackをVStackの中にネストするのは無意味です — 外側のVStackが内側のLazyVStackのすべての要素を即座に作成し、遅延ロードを無効にします。VStackをLazyVStackの中にネストするのは問題なく、遅延メカニズムを壊しません。LazyVStackを別のLazyVStackの中にネストすることは、ネストされたセクションでは許容されますが、パフォーマンスに注意してください:各レベルがジオメトリ追跡のオーバーヘッドを追加します。

LazyVStackの要素間に区切り線を追加するには?

SwiftUIはLazyVStackに組み込みの区切り線を提供していません。手動で追加してください:ForEachの各要素の後にDivider()を配置するか、各要素に.overlay(Divider(), alignment: .bottom)モディファイアを使用します。カスタム区切り線の場合は、Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3))を描画します。

まとめ

  • LazyVStack — 大規模なリストを効率的にレンダリングするためのSwiftUIの垂直遅延スタック。要素が可視領域に入ったときに作成されます。
  • LazyHStack — 同じ遅延ロードメカニズムを持つ、カルーセルや水平リスト用の水平版。
  • VStackとの違い — VStackはすべての要素を一度に作成しますが、遅延スタックは可視のもののみを作成し、大規模データセットで最大50倍のメモリを節約します。
  • pinnedViews — Section { header: }.sectionHeadersによる「貼り付き」効果のためのセクションヘッダー固定。
  • idによる識別 — 必須条件:ForEachは各要素に一意で安定したidを必要とします。
  • パフォーマンス — 1~3画面のプリフェッチバッファ、不要な再描画を防ぐEquatableView、非同期画像ロード。
  • ツールの選択 — カスタムデザインにはLazyVStack、組み込み編集とswipeActionsにはList。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください