📚

List それとも LazyVStack:SwiftUI におけるレイジーコンテナの選択

2024/07/15に公開

SwiftUIの世界では、ListLazyVStack は主要なレイジーコンテナとして、大量のデータを表示するための強力なサポートを提供します。しかし、これらは特定の状況で似たような動作をするため、開発者が選択に迷うことがあります。この記事では、これら2つのコンポーネントの特徴と利点を分析し、より良い選択をするための手助けをします。

注:この記事で言及する LazyVStack は、主に ScrollViewLazyVStack の組み合わせを指し、通常は ForEach を使用してデータを動的に提供します。また、ここで議論する LazyVStack の特性は、ほとんどの場合 LazyHStack にも適用され、一部は他の Lazy 系列のコンテナにも適用されます。具体的なAPIの使用詳細ではなく、全体的な比較と考察に焦点を当てます。

この原文は私のブログ Fatbobman's Blog に掲載されています。Swift、SwiftUI、Core Data、SwiftData に関する最新のアップデートや優れた記事をお見逃しなく。Fatbobman's Swift Weekly に登録して、毎週の洞察と貴重なコンテンツを直接メールボックスにお届けします。

内部実装:異なる起源、異なるアーキテクチャ

SwiftUIの進化の過程で、ListLazyVStack は異なる役割を果たし、それぞれ異なる位置付けを持っています。List はSwiftUIの最初のバージョンにおける初代レイジーコンテナであり、当時唯一の公式レイジーコンポーネントでもありました。そのため、後の多くのレイジーコンテナのデータロードプロセスの基盤となりました。一方、LazyVStack は、その他のレイジーコンテナ(例えば LazyHStackLazyVGridLazyHGrid など)と共に翌年登場しました。両者は特定の状況で似たような動作をするものの、その内部アーキテクチャは大きく異なります。

List は本質的に、AppleによるUIKit/AppKitコンポーネントの巧妙なラッピングです。iOS 13からiOS 15の期間、その基盤は UITableView に依存していましたが、iOS 16以降ではより柔軟な UICollectionView に切り替わりました。これに対し、LazyVStack および他の Lazy+ 系列のコンテナは、SwiftUIのネイティブ実装であり、特定のUIKit/AppKitコンポーネントに依存していません。

この根本的な実装の違いは、技術的な詳細だけでなく、性能、機能の豊富さ、カスタマイズの柔軟性、レイアウトロジックなど、さまざまな重要な点で異なる特性を生む原因となっています。これを理解することは、開発者がこれら2つのコンポーネントを選択し使用する際に非常に重要です。

スタイリングとプリセット:機能の豊富さ vs. シンプルな柔軟性

LazyVStack は、その親戚である VStack と同様に、レイアウトの本質機能に集中しています。SwiftUIの純粋なレイアウトコンテナとして、それは予め定められたレイアウトルールに厳密に従い、子ビューを順序よく配置しますが、余分な効果を追加することはありません。このシンプルな設計は、開発者に最大限の自由を提供します。

対照的に、List の設計哲学は全く異なります。List は単なるコンテナではなく、多機能なUIコンポーネントです。List は多くのプリセットビジュアルテンプレートを備えており、開発者は listStyle を通じて簡単に異なるスタイルに切り替えることができます。しかし、List の利点はビジュアル効果だけにとどまりません。以下のような独自のインタラクティブ機能も提供します:

  • スワイプ操作(swipeActions)は List の子ビュー専用
  • ForEachonDelete および onMove 機能は List 内でのみ有効
  • ジェスチャーベースのアイテム並べ替えを可能にする編集モードのサポート

スワイプメニューやシステムアプリ風の削除と移動編集機能が必要なシーンでは、List が最適です。List はコードがシンプルであり、Appleの全エコシステムに対して深く最適化されています。また、List の豊富な構造方法(ForEach とデータバインディングの統合を含む)は、特に初心者やプロトタイプ開発において、開発効率を大幅に向上させます。

しかし、List のプリセット機能には一定の制約もあります。SwiftUIの更新に伴い、List はより多くのプリセットテンプレートを取得しましたが、iOS 18時点でAppleはまだ List スタイルの完全なカスタマイズ機能を開放していません。対照的に、LazyVStack は白紙の状態のようなもので、プリセットモードはないものの、開発者の創造力に無限の可能性を提供します。

総じて、Appleはこれら2つのコンポーネントに対して全く異なる位置付けをしています。List はデフォルトのスタイルと動作を備えた多機能コンテナであり、LazyVStack は純粋で柔軟なレイアウトツールです。

レイアウトとカスタマイズ:柔軟性と制約のバランス

LazyVStack はSwiftUIのレイジーコンテナとして、特に子ビューの高さを処理する際に独自の特性を持っています。VStack と異なり、LazyVStack は子ビューに明確な高さが指定されていない場合、子ビューの理想サイズを採用します。この特性は実際のアプリケーションで非常に明確に表れます:

struct ContentView: View {
  var body: some View {
    LazyVStack {
      Rectangle()
    }
  }
}

上記のコードでは、Rectangle は高さ10の矩形(Shapeのデフォルトの理想サイズ)として表示されます。これは VStack で表示される場合の全ての利用可能なスペースを埋める矩形とは異なります。

このサイズ決定ロジックは、ScrollView のスクロール方向におけるレイアウト方法と一致します。VStack 内にネストされていても、Rectangle は高さ10を保持します:

ScrollView {
  VStack {
    Rectangle()
  }
}

SwiftUIのレイアウトサイズ決定メカニズムについて詳しく知りたい方は、SwiftUI レイアウト — サイズ をお読みください。

LazyVStack は純粋なレイアウトコンテナとして、開発者に非常に高い自由度を提供し、デザインスタイルをより正確に再現することができます。一方、List の表示は選択したスタイル、コンテナ環境、および実行プラットフォームに依存し、開発者が List のスタイルを介入する能力は限られています。そのレイアウトもよりモジュール化されたものに傾いています。

List の外観と動作を調整するには、主にAppleが提供する専用のビュー修飾子に依存します。これらの修飾子はSwiftUIのバージョンアップに伴い次第に豊富になっていますが、初期のSwiftUIバージョンでは、複雑なレイアウトを実現することが難しいか、または回避策を用いる必要がある場合があります。

List の実装の違いにより、行高が動的に変化するシーンでは制約が存在します。例えば:

struct ContentView: View {
  @State var high = false
  var body: some View {
    List{
      Toggle(isOn: $high.animation()){}
      Rectangle()
        .frame(height:high ? 200 : 100)
    }
  }
}

list-row-animation-issue

このような動的な高さ変化のシーンは、List では極力避けることをお勧めします。

さらに、List は行ビューのトランジションアニメーションタイプにも制約があります。特別なアニメーションやトランジション効果が必要なシーンでは、LazyVStack がより大きな柔軟性と優れたパフォーマンスを提供します。

他のコンテナとの連携:文脈感知の利点

一部の開発者は、List の実装に不満を持ち、それが特定のニーズを完全に満たさないと感じる場合、自身でUIKit/AppKitコンポーネントをラッピングして、よりニーズに合った代替品を作成しようと考えるかもしれません。このアプローチは確かに特定の状況でより適したソリューションを提供することができますが、Appleが List を多プラットフォームと特定のニーズに適応させるために行った多大な努力を見過ごしてはいけません。

SwiftUIにはまだ公開されていない重要なメカニズムがあり、公式のコンテナやコンポーネントは自分が置かれている環境を知覚し、異なる文脈に応じて表示スタイルを自動的に調整する能力があります。このユニークな能力は、ListLazyVStack に対して持つ顕著な利点の一つです。

List はナビゲーションコンテナとの組み合わせにおいて完璧に動作します:

  1. サイドバー(Sidebar)などの特殊表示モードをサポート
  2. ナビゲーションコンテナは、List にバインドされたデータソースと行ラベルを優れたサポート
  3. 特定のコンポーネント(例えば NavigationLink)が List 内で独特なスタイルを呈示

これらの特性は、開発工数を大幅に削減するだけでなく、LazyVStack では実現が難しいインタラクティブな体験を提供します。

List とナビゲーションコンテナの連携について詳しく知りたい方は、SwiftUI 4.0 の全新ナビゲーションシステム および SwiftUIで適応型プログラムナビゲーションソリューションを作成する をお読みください。

しかし、このような文脈感知とデフォルトの動作は、時には課題ももたらします:

  • デフォルトでは、List は行ビュー内の一つの Button 要素の操作にのみ応答します(buttonStyle を調整することで解決可能)
  • 初期のSwiftUIバージョンでは、プログラム化されたナビゲーション能力が不十分であり、NavigationLink スタイルを制御することが難しかった

総じて、システムコンポーネントの自動感知能力を最大限に活用し、システムスタイルに一致するインターフェースを作成したい場合、List は明らかに優れた選択です。それはユーザーに馴染みのあるインタラクティブ体験を提供し、開発者が異なるプラットフォーム間でより高いコード再利用率を達成するのを助けます。

スクロール制御:ネイティブ vs. 回避策

最近のSwiftUIのバージョン更新では、Appleはスクロール制御能力を大幅に強化し、一連の新しいAPIを導入しました。しかし、これらの新機能は主に ScrollView に向けられており、List の進展は比較的遅れています。現在、List に対する公式のスクロール制御手段は ScrollViewReader に限定されています。

それでも、List の基礎実装が成熟したUIKitコンポーネントに基づいていることから、開発者には回避策が提供されています。SwiftUI-Introspect などのサードパーティライブラリを使用することで、開発者は基底のUIKitコンポーネントのAPIに直接アクセスし、より多くの制御手段を実現できます。この方法はスクロール制御だけでなく、カスタム表示スタイルにも適用されます。

とはいえ、iOS 17以降では、ScrollViewLazyVStack の組み合わせがスクロール制御において List を上回る能力と便利さを提供しています。LazyVStack のレイアウトの柔軟性とアニメーションサポートの固有の利点を考慮すると、子ビューの正確なスクロールや子ビューの位置に基づいて異なるビジュアル効果を実現する必要がある場合、LazyVStack が明らかに優れた選択となります。

スクロール制御APIの最新の発展について詳しく知りたい方は、SwiftUI 5 の ScrollView の新機能を詳しく理解する および SwiftUI スクロール制御 API の発展の歴史と WWDC 2024 の新しいハイライト をお読みください。

パフォーマンス:挑戦とトレードオフ

SwiftUIが登場してから六年経ちましたが、大規模なデータセットを処理する際の ListLazyVStack のパフォーマンスはまだ向上の余地があります。中規模のデータセットに直面する場合でも、内部実装の違いから、両者は異なるパフォーマンス特性を示します。

LazyVStack は本質的にレイジーロード能力を備えた VStack であり、子ビューの高さの総和とスペースを保持する完全なコンテナの高さを管理します。レイジーロードを実現するために、それは動的に高さを推定し、可視領域の近くにある子ビューの数と高さに基づいて全体の高さを推定します。このメカニズムは、以下のような顕著な問題を引き起こします:

  1. 特定の位置に急速にスクロールする場合、その位置までの全ての子ビューの高さをインスタンス化して計算する必要があり、明らかなパフォーマンス低下を引き起こす可能性があります。
  2. 子ビューの高さに大きな差がある場合、急速なスクロールや大幅なジャンプは計算効率の問題で白い画面現象を引き起こす可能性があります(必要な全ての子ビューをタイムリーに計算できない)。

これに対し、List はSwiftUI層で完全なコンテンツ高さの概念を管理しません。急速にスクロールや大幅なジャンプを行う場合、必要な子ビューをインスタンス化し、高さを計算することを選択し、スクロールやジャンプの効率を著しく向上させます。

注目すべき点は、Listid 修飾子を使用すると、子ビューがレイジーロード能力を失う可能性があることです。対照的に、LazyVStack にはこの問題がありません。そのため、List にデータソースを提供する際は、データ型が IdentifiableHashable プロトコルの両方に準拠するようにし、id 修飾子をスクロール制御の位置ラベルとして使用することを避けることをお勧めします。

総じて、同じデータ量で List は通常 LazyVStack よりも高い効率を示します。

パフォーマンス最適化戦略について詳しく知りたい方は、SwiftUI でレイジーコンテナを使用する際のいくつかのコツと注意点 および SwiftUI の List で大規模データセットを表示する際のレスポンス効率を最適化する をお読みください。

まとめ

ListLazyVStack の設計哲学には、存在即合理 という観点が完全に反映されています。これらのコンポーネントはそれぞれの特性を持ち、SwiftUI開発者に異なるソリューションを提供します。どちらを使用するかを選択する際には、以下の重要な要素を総合的に考慮する必要があります:

  1. パフォーマンス:特に大幅なジャンプや子ビューの高さ差が大きいシーンに注意
  2. スクロール制御能力:精度と子ビューの位置感知能力
  3. レイアウトの柔軟性:複雑なUIデザインへの適応能力とアニメーションおよびトランジションの互換性
  4. プリセット機能のニーズ:システム提供の組み込み機能を利用するかどうか
  5. クロスプラットフォームの互換性:Appleエコシステムの異なるプラットフォームでのパフォーマンス
  6. 他のSwiftUIコンポーネントとの連携能力:特にナビゲーションとデータバインディングにおいて

一刀両断の解決策はなく、最適な選択は具体的なプロジェクトのニーズと開発シーンに依存します。これら2つのコンポーネントの利点と欠点を深く理解することで、開発者は具体的な課題に直面したときに賢明な判断を下すことができます。

実践においては、両者を組み合わせて使用する必要がある場合や、異なるページでそれぞれを使用して各々の長所を発揮する場合もあります。アプリケーションの具体的なニーズ、目標ユーザー群、パフォーマンス要件などの要素に基づいて、これらのツールを柔軟に選択し使用することが鍵です。

SwiftUIの絶え間ない発展に伴い、これらのコンポーネントの能力や適用シーンは変化する可能性があります。新機能やベストプラクティスに注目し続けることで、SwiftUI開発において常に効率的かつ革新的であり続けることができます。

Discussion