ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUI の Stack, Grid, Outline
改良されたStackと新しいList、Outlineビューを使って、SwiftUI Appで詳細データをより素早く効率的に表示しましょう。iOS, iPadOSには新登場となるOutlineは、StackやListとともに動く階層データを表現できる新しいマルチプラットフォームツールです。SwiftUIの新しく改良されたツールを使用し、テーブルビュー利用時により多くのコンテンツを表示する方法、スムーズなスクロールで応答性の高いStackを作成する方法、vStackが提供できる以上のものを必要とするコンテンツのListビューの構築方法をお伝えします。またDisclosureGroup同様、Gridビューでより多くのレイアウトオプションも利用可能です。 この動画を最大限に活用するには、事前にSwiftUI for 2020の新情報がすべて網羅されている"SwiftUI App Essentials"をご覧ください。SwiftUIでコーディングすることが初めてであれば、2019年の”SwiftUI Essentials"もご覧いただくことをお勧めします。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC20
WWDC19
-
ダウンロード
こんにちは WWDCへようこそ
“SwiftUIでStack, Grid, Outlineを使う” 私はコディー SwiftUIに携わるエンジニアです セッションの後半で 同僚のカートが加わります SwiftUIにはビューの集まりを 水平や垂直の並びで整列するための 様々なレイアウトプリミティブが 内蔵されています これらのプリミティブを使用するだけで 基本的なレイアウトのニーズに対応できます さらに複数を互いに結合させて カスタム動作を示す複雑なビューを 構築することも可能です macOSの新しいNotification Centerは SwiftUIと共に実装されています それを このコンポジションプロセスにおける 最適な例の1つとして利用する予定です Simple StackとGridは連携して機能し 階層やアラインメントと スペーシングを使用して多数の情報を組織化し 結果として仕上がりを美しく 使いやすいものにします 独自のアプリケーションを開発する場合 同じ条件で考えることを推奨します SwiftUIのレイアウトプリミティブは コンポジションを念頭において設計されました 一般に1つのシンプルタイプでは 必要なことをすべて処理できない場合 先に進むためには相補的な動作を行う 別のシンプルタイプと結合する必要があります ここではSwiftUIのレイアウトプリミティブの ファミリーに追加されたいくつかの 新機能について話をします 最も基本的なタイプ 水平と垂直のStackのレビューから始めて ゆるやかに成長するグリッドレイアウトを 作成するための新しいペアを紹介します さらに 階層データの プレゼンテーションを可能にする― 既存のListタイプの新しい 機能についても解説します
最後にカートがOutlineとFormについて 詳しく解説し ユーザーインタフェイス制御の プログレッシブ表示のテクニックを紹介します Stackの話から始めましょう これはSwiftUIで最も簡単な レイアウトプリミティブです Stackについて話すためには 初めにサンドイッチの話をする必要があります “Introduction to SwiftUI”を 視聴済みでしたら ジェイコブがサンドイッチを 作るためのアプリケーションに 力を注いできたことをご存知でしょう 私はサンドイッチ通で 特に記憶に残った― ランチのショーケース写真を ジェイコブのアプリケーションの ギャラリービューと合わせたら 面白くなるだろうと考えました 使用するデータモデルはシンプルで ID 名前 評価を表す星と ギャラリー用のヒーローイメージだけです
ギャラリーに個別のサンドイッチを表示する ビューも 同じくらいシンプルです リサイズ可能なヒーローイメージの表示と
サンドイッチの情報を含む オーバーレイが追加されているだけです
それぞれのヒーローイメージを オーバーレイするBannerViewは VStackを使用してサンドイッチのタイトルと 評価の星のインジケータを配置します 評価の星は画像を ただ水平に並べたものです
初めの実装はかなり単純です ギャラリーはサンドイッチビューの 垂直スタックを使用して表示しています サンドイッチリストは 写真を撮ればとるほど増えていくので すべてのサンドイッチを列挙して それぞれのビューを作成するForEachビューを 組み込む必要があります さらに Stackは自動で スクロールはしないので すべてをScrollViewの中に 入れておく必要があります ここまではうまくいきましたが サンドイッチ写真のバックカタログの ローディングに取りかかろうとして 問題があることに気付きました ギャラリーに表示する写真が多くなるほど 表示の際にスクリーンが反応するまでの 時間が長くかかるようになります 私が求めているのは増えた分を 逐次構築していくLazy Stackです そうすれば 最初は画面に映る分だけ 画像を表示すればすみます 残りはユーザーがギャラリーを スクロールするのに合わせて オンデマンドでロードできます この問題に直接対処できる2つの 新しいSwiftUIスタックタイプを紹介しましょう LazyVStackと LazyHStackです Lazy Stackは対応するVStackや HStackによく似ていますが 表示する必要ができた分だけコンテンツを 逐次表示する点が異なります これこそが私の求めるものです ビューはメインスレッドによる全単一画像の ローディングと測定をブロックしませんし アプリケーションのメモリーフットプリントが 不必要に大きくはなることもありません VStackをLazyVStackに 置き替えるだけで
ギャラリーは増えた分を 逐次ロードするようになります
もう1つ 指摘しておきたいことがあります “rating view”の定義を 思い出してください HeroViewのギャラリーを定義する 垂直スタックは このスクリーン上の 唯一のスタックではありません
各HeroViewには 評価の星のインジケータを レイアウトするための 個別の水平スタックと ヒーローイメージ上に評価を オーバーレイするためのZStackがあります では 外側のスタックを Lazyにしたので こちらもLazyにすべきでしょうか?
この場合 答えはノーです 垂直スタックは Lazyにしたいと思っていました なぜなら スクロールしなければ コンテンツの大部分は見えませんが スクロールした時に すべてが表示されるのに時間がかかるのは 好ましくないからです 一方で HeroViewの中にあるスタックを Lazyにしても 実際にメリットはありません ビューがスクリーン上に到達すると コンテンツの すべてが即座に見えるようになります そのため コンテナの デフォルト動作に関係なく すべてを一度に ロードしなければなりません 原則として どのタイプのスタックを 使用するかが決まっていないなら VStackか HStackを使用します Lazy Stackは計測機器を用いた プロファイリングの後で判明した性能の ボトルネックを解決する 手段として採用するようにします 今度は新しい一連のタイプ Lazy Gridについて話したいと思います サンドイッチギャラリーに戻りましょう
iPhoneでの見え方は気に入っていますが より大きな画面ではどうなるでしょうか iPadに移動させて確認しましょう
同じものですが 少し大きいようです 私が期待したとおりの見え方ではありません 画面の活用可能な範囲が広がった時 私が本当に望んでいるのは より多くの サンドイッチを画面に表示することです 単一カラムの画像を マルチカラムのグリッドに変更できれば サンドイッチ全体の密度を かなり上げることができます SwiftUIのレイアウトプリミティブファミリーに 追加された2つのタイプを紹介します LazyVGridと LazyHGridです これらのタイプは Stackを 使用した時と同じようにコンテンツの グリッドを逐次増やしていきます LazyVGridを使用すれば 容易にマルチカラムレイアウトを実装し ビューのサンドイッチ 密度を上げることができます どのように機能するか見てみましょう
これは以前に見たものと同じLazy Stackを iPad用にスケールアップしたものです これを更新してサンドイッチのカラムが 1つではなく 3つになるようにします 前例との大きな違いは レイアウトコンテナです LazyVStackの代わりに LazyVGridを使用し 数値の集まりを渡して SwiftUIに グリッドのカラムの幅の 計算方法を伝えます 少し補足します カラムの説明からは離れますが 私はビュービルダーを経由して グリッドを形作る個々のビューを 生成するというやり方で スタックを定義するのと 同じようにしてグリッドを定義します グリッドのカラムについて解説するため GridItem値のアレイを作成します 各項目は個々のカラムの幅の 計算方法を示しています ここでは3つのカラムを定義しています グリッドアイテムはデフォルトで フレキシブルになっているので この配置ではグリッドは 均等な幅のカラムで満たされます
これは横向きにしても同じです カラムの数は同じですが 少し幅が広くなっています さらにグリッドレイアウトは 利用可能なスペースに対応して 可変数のカラムを作成できます ここでは例として 指定された 最小のカラム幅を維持しながら 均等な幅のカラムを 可能な数だけ生成する― 適応型のGridItemを 宣言しています このやり方はカラムを追加する余地がある 横向きモードに適しています 適応型のグリッドアイテムは ウインドウを 任意にリサイズできる macOSにも適しています これらの新しいプリミティブの表現力には 本当にワクワクします カートに引き継ぐ前に話しておきたい 最後のトピックはListです Listは まさに基本のレイアウト プリミティブ以上のものです それはインタラクティブで 選択管理とスクロールをサポートします リストのコンテンツは常に Lazyにロードされます 皆さんがどうかはわかりませんが この時点で私はサンドイッチは もう十分にいただきました カートが取り組んでいる“ShapeEdit”という 新しいアプリケーションを見てみましょう ShapeEditはmacOSやiPadOSとiOS上で動作する ドキュメントベースのアプリケーションです ズームしていくとShapeEditに ウインドウのサイドバービューが現れます ここにListを使用して キャンバスの シェイプを列挙しています
今 キャンバスには グラフィックスのアレイがあり― そのグラフィックスアレイを使用して サイドバーに数行の コンテンツを配置し シェイプのフラットリストを 生成しています とてもクールですね このアプリケーションを操作するのは 楽しかったので シェイプを収集するフィーチャーを グループに追加することに とても興味をそそられました グループが別のグループを含むことも 可能なので フラットリストはエレメントのディープツリーを 恣意的に示す必要があります Listを完全なものにするために 新しいフィーチャーを追加しました その話をするのがとても楽しみです リストをアウトラインに変えるには データツリーを横断する方法を リストに伝える必要があります 新しいinitializerを使用して children key pathを グラフィックモデルで提供し 残りはSwiftUIが行います この1つの変更でサイドバーはシェイプの 完全な階層を示すようになりました 非常にすばらしい 想像されているように 内部では このアウトラインの 作成を自動化するための 興味深い作業が数多く行われています では カートに話を引き継ぎます 彼はListが使用しているものと 同じツールを使用して プログレッシブ開示を自分のUIに 実装する方法について解説します カート
ありがとう コディー リストをアウトラインに変換するのは とてもクールなやり方です どのように機能するか詳しく解説します この詳細はとてもすばらしいもので 皆さんのアプリケーションでも 部分的に利用できると思います コディーはchildren key pathを Listに渡して ShapeEditで サイドバーにグラフィックスの アウトラインを表示する 方法を解説しました 私はアプリケーションで複数のキャンバスを サポートするのがクールだと考えており モックアップをスケッチしました このモックアップは各キャンバスで 別のセクションを使用しており それぞれのセクションには 別々のアウトラインがあります このようなカスタムアウトラインを 実装する方法について見ていきましょう コディーが述べたように Listは選択管理を支援する ハイレベルの構造なので その話を続けます Listでは キャンバスにて 反復を行う場合は ForEachを使用します それぞれのキャンバスごとに Sectionを使用してキャンバスの 名前を示すヘッダを追加します そして最後の Sectionの コンテンツがSwiftUIの新しいビュー OutlineGroupです ForEachに類似していますが フラットな集合を繰り返すのではなく ツリー構造データを横断します ここに グラフィックスのアレイと children key pathを指定します OutlineGroupは各項目が GraphicRowであるアウトラインを生成します
Xcodeに切り替えて 実際に どのように動作するかを確認します プレビューにはグラフィックスの アウトラインが表示されています SwiftUIのアウトラインはmacOSだけでなく iOSでも機能します iPadやiPhoneで強力な内蔵のアウトライン機能を 使えるのはとてもすばらしいことです Live Previewで これらのグループが どのように機能するかを見てみましょう
ディスクロージャーインジケータをタップして グループを拡張したり 折りたたむことができます ビューを更新してすべての キャンバスを表示するようにします まずはListに OutlineGroupを追加して その中にGraphicsRowが入るようにします
さらに この最初の2つの引数を ListからOutlineGroupへ移動します
プレビューがまだ変化していないことに 注意してください Listのすぐ内側のOutlineGroupは childrenパラメータを 使用するListと同じものです 次に ビューを変更して グラフィックスの 代わりにキャンバスを使用するようにします Commandをクリックし Repeatを選択して このOutlineGroupをForEachの中に入れます
さらに引数を自分のモデルの キャンバスに置き替え
パラメータの名前を変更します
最後に 単一のキャンバスではなく グラフィックス全体を反復するように OutlineGroupを変更します これで すべてのキャンバスの グラフィックスが表示され連動します セクションヘッダーをいくつか追加します Shift+Command+Lを押して ライブラリーをオープンし Sectionをフィルタリングします Sectionをドラッグして入れ ヘッダーにキャンバス名を表示させます
SidebarListStyleを 使用しているので iOS 14で導入されたこれらの 美しい太字のヘッダを利用できます これらも拡張したり 折りたたむことができます
とてもクールなやり方ですね 階層構造のListと OutlineGroupに加えて SwiftUIは情報のプログレッシブ表示を行う 新しい便利なツールを MacとiOSに提供します 時々 コントロールや他の情報の 表示や非表示を要求する― アプリケーションが通常の階層に 従わないことがあります このInspectorのポップオーバーのように このようなケースに対応した― 第三の新しいツール DisclosureGroupを紹介します DisclosureGroupは ディスクロージャーインジケータと ラベルおよびコンテンツを提供します ユーザーがディスクロージャーインジケータを タップまたはクリックすると コンテンツが表示されます もう一度タップまたはクリックすると コンテンツは非表示になります 使い方を見てみましょう ここに Inspectorがあります fill 影 テキストプロパティを調整する コントロールが含まれています そのすべてがFormの中に組み込まれています Formはこのようなコントロールの 集合に対応した最適な選択です macOSでも 新しいSettings Scenesで Formを使用できます Inspectorがアプリケーションで どのように機能するか見てみましょう
ShapeEditは iPadでは とてもうまく機能します シェイプを選択して Inspectorをオープンします
色を変更して 影を追加します
さらに シェイプも変更できます
コードに戻りましょう
このInspectorはうまく機能していますが 少し煩雑になっています もう少し整理できないか確認しましょう 最初に これらすべてのfillコントロールを DisclosureGroupの中に入れます
ライブラリーから DisclosureGroupを持ってきます
タイトルをFillに設定します
fillコントロールがInspectorでまとめて 折りたたまれていることに注意してください アウトラインと同じように ディスクロージャーグループも 拡張や折りたたみが可能です
このグループはアイコンを使用できます それにラベルを使用します このコンビニエンスプロパティを削除して ラベルにtrailing closureを追加します
任意のビューをここに配置できますが 新しいLabelタイプは タイトルとアイコンを意味的に 結合する便利な方法です
ここでSF Symbolイメージを使用します 気に入っているのは rectangle.3.offgrid.fillです
うまく表示されました 影とテキストコントロールも 同じように処理します
これらの処理により Inspectorの 見た目が良くなりました もう1つ変更したい ものがあります ユーザーはfill設定を 調整することが多いと思うので Inspectorをオープンした時に それらが表示されるようにします 早速やってみましょう SwiftUIのディスクロージャーグループを 拡張を制御するブールプロパティに 結合することができます 信頼できる情報源として機能する ブールステートを追加します
そして デフォルトをtrueにします
DisclosureGroupを構成して 新しいステートに結合させます
これで fillコントロールはデフォルトで 拡張された状態になります ここまでアウトラインとディスクロージャー グループを使用してアプリケーションの情報の プログレッシブディスクロージャーを 管理する方法について解説してきました 講演を締めくくる前に OutlineGroupが実際にどのように 機能しているかを見てみましょう これはコンポジションの原則を示す すばらしい例です アウトラインとディスクロージャーグループを 使用するのに このビットを理解する必要はありませんが 皆さんもクールな方法だと 感じると考えています ここでは グラフィックスの集合に OutlineGroupを設定します SwiftUIは OutlineGroupを 同じグラフィックスの集合の ForEachに拡張します ForEachのbodyは DisclosureGroupです それぞれのDisclosureGroupのラベルは オリジナルの集合の 単一エレメントを用いて生成されますが 各DisclosureGroupのコンテンツは 別のOutlineGroupであり ここではその単一エレメントのchildrenが 対象であることに注意してください このunwindingプロセスは childrenのない グラフィックが見つかるまで継続されます ただし SwiftUIは誰かがオープンした ディスクロージャーグループの コンテンツを評価するだけなので 実際に実行されるのは 最小量のプロセスだけです 前に述べたように アウトラインとディスクロージャーグループを 使用するのに このunwindingを 理解する必要はありませんが OutlineGroupを使用可能にするための 再帰とコンポジションの 組合せはすばらしいと思います このSwiftUIのツールのツアーが 皆さんに役立つことを期待しています HStackとVStackは 固定された一連のアイテムの配置を 制御するのに適したツールであると 考えています 一方 新しいLazy Stackは 可変数で 数量が大きくなる可能性がある― 一連のアイテムを表示するスクロールビューで 非常にうまく機能します Lazy Gridはグリッドに集合を表示する 使いやすく斬新な方法を提供します Listは非常に強力で 選択 スクロールと コンテンツの Lazyローディングのサポートを提供しますが さらに今年は階層データの 表示も追加されました Inspectorの例でお見せしたように Formは設定や 他のリストのコントロールに使用します 最後に 新しいアウトラインと ディスクロージャーグループは 情報のプログレッシブ表示を アプリケーションに合うよう― 適切に調整する能力を提供します アプリケーションにデータを 適切に表示する方法についての詳細は developer.apple.comから ShapeEditのコードをダウンロードできます また アプリケーションでSettings Sceneを 作成する場合の詳細については “App Essentials in SwiftUI”を モデルとビューの 結合に関する詳細については “Data Essentials in SwiftUI”を ご覧ください もっとサンドイッチについて知りたい方は WWDC 2020の“Introduction to SwiftUI”を ご覧ください ご視聴ありがとうございます ごきげんよう
-
-
2:08 - Sandwich and HeroView
// Sandwich model and gallery item view struct Sandwich: Identifiable { var id = UUID() var name: String var rating: Int var heroImage: Image { … } } struct HeroView: View { var sandwich: Sandwich var body: some View { sandwich.heroImage .resizable() .aspectRatio(contentMode: .fit) .overlay(BannerView(sandwich: sandwich)) } }
-
2:26 - Sandwich Info Banner
// Banner overlay view for sandwich info struct BannerView: View { var sandwich: Sandwich var body: some View { VStack(alignment: .leading, spacing: 10) { Spacer() TitleView(title: sandwich.name) RatingView(rating: sandwich.rating) } .padding(…) .background(…) } }
-
2:34 - Sandwich Rating View
// Sandwich rating view struct RatingView: View { var rating: Int var body: some View { HStack { ForEach(0..<5) { starIndex in StarImage(isFilled: rating > starIndex) } Spacer() } } }
-
2:39 - Scrollable Stack of HeroViews
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … ScrollView { VStack(spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
3:53 - Scrollable Stack of HeroViews
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … ScrollView { VStack(spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
3:57 - Scrollable Lazy Stack of HeroViews
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … ScrollView { LazyVStack(spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
6:09 - Scrollable Lazy Stack of HeroViews
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … ScrollView { LazyVStack(spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
6:18 - Three-Column Grid of Sandwiches
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … // Define grid columns var columns = [ GridItem(spacing: 0), GridItem(spacing: 0), GridItem(spacing: 0) ] ScrollView { LazyVGrid(columns: columns, spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
7:13 - Adaptive Grid of Sandwiches
// Fetch sandwiches from the sandwich store let sandwiches: [Sandwich] = … // Define grid columns var columns = [ GridItem(.adaptive(minimum: 300), spacing: 0) ] ScrollView { LazyVGrid(columns: columns, spacing: 0) { ForEach(sandwiches) { sandwich in HeroView(sandwich: sandwich) } } }
-
8:47 - Outline of GraphicRows
struct GraphicsList: View { var graphics: [Graphic] var body: some View { List( graphics, children: \.children ) { graphic in GraphicRow(graphic) } .listStyle(SidebarListStyle()) } }
-
9:52 - Customizing your outlines
// Customizing your outlines List { ForEach(canvases) { canvas in Section(header: Text(canvas.name)) { OutlineGroup(canvas.graphics, children: \.children) { graphic in GraphicRow(graphic) } } } }
-
13:10 - DisclosureGroup
// Progressive display of information Form { DisclosureGroup(isExpanded: $areFillControlsShowing) { Toggle("Fill shape?", isOn: isFilled) ColorRow("Fill color", color: fillColor) } label: { Label("Fill", …) } … }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。