-
SwiftUIレイアウトガイド
SwiftUIにおけるレイアウトの基本的なコンセプトをご確認ください。システムとシームレスに連携する柔軟な動的ビューを構築し、ワークフローを効率化する重要なデバッグ技術をご紹介します。
このセッションは、「Appleに相談」アクティビティである「SwiftUIの基礎:SwiftUIで優れたアプリを構築」の一部として実施されました。ビデオ全編を視聴して、詳しい情報や関連セッションをご確認ください。リソース
-
このビデオを検索
おはようございます。みなさん、こんにちは。 私はキャットと申します。Appleで 開発ツールエンジニアを務めております。
ここ数年、 Apple Watch向けの「Vitals」 アプリや、iPhoneおよび iPadの「ヘルス」アプリにおける睡眠機能などの プロジェクトに携わってきました。 そうですね、これらは直感的に操作でき、 見た目も美しいものである必要があります。 手首を一瞥するときでも、 スマホ画面を見るときでも、
こうした仕事を通じて、 確固たるUIレイアウトの基礎こそが、 それを実現するための土台であることを 学びました。
本日は、SwiftUIのレイアウトにおける 普遍的な構成要素となる 原則についてお話しします。 どのプラットフォームで開発を行う 場合でも、これは変わりません。 まず、レイアウトにおいて特に 重要なSwiftUIの概念的な要素を改めて 確認し、続いてSwiftUIの レイアウトの仕組みについて 解説します。
その後、SwiftUIを使用して 予測可能な結果を得る 方法の具体例をご紹介いたします。
それでは、基本となる 「ビュー」から始めましょう。
今朝、Leahさんが説明されたように、 SwiftUIのビューとは、 画面に何が表示されるかを記述する 軽量なテンプレートです。
ビューは、位置、サイズ、 階層構造を表すテンプレートです。
SwiftUIは、画面上の要素を記述するために 軽量なビュー構造体を作成し、 更新のたびにそれらを破棄して 新しい構造体を作成します。
SwiftUIの更新エンジンは、変更された UI部分のみを更新するため、 状態の変化に基づいて 予測可能で効率的な結果が得られます。
この効率性は、SwiftUIのローカル更新モデルに 由来しています。 ビュー階層の一部で変更があっても、 関係のないサブツリーには波及しません。 そのため、影響を受けた サブビューのみが再レンダリングされます。
そしてSwiftUIでは、 ビューテンプレートが、配置や サイズ設定からレンダリング、 状態の更新に至るまで、すべてを制御します。
ビューとは何か、そしてSwiftUIの更新エンジンがどのように 機能するかについて 説明しましたので、次に、 SwiftUIがレイアウトを実行する際に、 これらの概念がどのように 組み合わさるかの仕組みについてご説明します。
レイアウトの仕組みを解説するにあたり、 ウィッシュリストアプリで作成した 「トリップカード」に焦点を当てていきます。
この「トリップカード」は、 SwiftUIがさまざまな状態に 基づいて動的なレイアウトをどのように 処理するかを示す、 素晴らしい例となっています。
SwiftUIにおいて、レイアウトは 状態の関数です。 ここで言う「状態」とは、 atStateプロパティラッパーのことを 指すわけではありません。 ここでは、例えば「Cali
coastal trails」といった旅行の 名前や、旅行の画像のURLなど、
一般的な状態のことを指しています。 SwiftUIは本質的に、 レイアウトから状態を抽象化しています。
これは他のUIフレームワークでも可能ですが、 ある程度の労力と厳密な管理が必要となります。 しかし、この抽象化は SwiftUIの不可欠な部分です。
状態は、UIを動的に 制御することでレイアウトを駆動します。
また、状態は、画面に 表示される文字列や画像など、 各ビュー内のコンテンツを埋める 役割も果たします。
他のフレームワークでは、 状態が変化した際に、 UIの特定の部分を再レイアウトするために 手動での更新が必要となります。
SwiftUIでは、状態が変化すると、 影響を受けるビューテンプレートが 自動的に再生成され、 常に最新の状態が保たれます。
それでは、ここに示されているように、 ウィッシュリストアプリの旅行カード用の レイアウトをどのように 構築したかについて説明します。
ここで、レイアウトを状態から 抽象化する方法について説明します。
旅行カードは、無地の暗い 背景の上に表示されます。
このカードには、旅行を表す 画像と旅行名のテキストビューを含む
VStackがあります。
旅行画像ビューには、アクティビティの 数を表示するオーバーレイがありますが、
これはアクティビティの数が ゼロより大きい場合のみ表示されます。 そうでない場合、テキストは生成されず、 SwiftUIはアクティビティのオーバーレイを レンダリングしません。
ビューの状態は、モデル内の「トリップ」 によって個別に制御されます。
トリップモデルには、トリップのタイトルとなる 名前文字列が含まれています。
また、トリップ用にどの画像をレンダリングするかを 決定するための写真URLも含まれています。
そして最後に、アクティビティの 辞書が含まれています。 この辞書内のアクティビティの数が、 アクティビティオーバーレイを表示するかどうかを 決定するために使用されます。
これは、状態がSwiftUIのレイアウトを 制御し、その値がどのように 機能するかを示す簡単な例です。 この例では、アクティビティの数が ビューのレイアウトをカスタマイズするために 使用されています。
次に、SwiftUIにおける レイアウトの仕組みについて説明します。
SwiftUIのレイアウトの基本的な前提は、 コンテナがサブビューに対して サイズを提案するということです。 この提案されるサイズは、 コンテナビューおよびそれより 上位のすべてのコンテナビューによって 制約されます。
各サブビューは、これに応じて 自身の優先サイズを計算し、それを報告します。
SwiftUI では、ビューごとに 優先サイズが異なります。
カラービューは、提供された スペースを常にすべて占有します。 利用可能なスペースを埋めるように拡大します。
画像ビューは、たとえ 提供されたスペースが少なくても、 常にその全サイズを占有します。 つまり、画像は包含する ビューの外にはみ出す可能性があります。
これを上書きするには、 resizable修飾子を使用してください。 そうすることで、割り当てられた スペースをすべて確保するようになります。 カラービューと同様に、ここにある 海岸の花のように、押しつぶされたように 見えてしまう場合でも同様です。
画像の元の縦横比を維持するには、 コンテンツモードを「fit」 または「fill」に設定してください。
テキストは、省略記号を表示するのに 十分なスペースを最低限確保します。 テキストは、そのコンテンツに 必要なスペースのみを確保します。
レイアウトとSwiftUIは 階層的に処理されます。
コンテナビューは、各サブビューに 対してサイズを提案します。
提案されるサイズが 特定されている場合を想定しましょう。 例えば、このオレンジ色のボックスは、 提案がどのようなものになるかを示しています。
コンテナビューがアプリの ルートビューである場合、 提案されるサイズはデバイスのサイズ、あるいは フレーム修飾子を適用した場合、提案される サイズはフレームのサイズとなります。
一方、提案されるサイズが、 一方または両方の軸で
未指定の場合もあります。
これは、コンテナビューが、 サブビューがまったく 制約を受けなかった場合にどれだけのスペースを 必要とするかを知りたいことを意味します。 これがサブビューの理想的なサイズです。
各サブビューは、提案されたサイズに基づいて、 自身の優先サイズを返します。
これは、他のビューシステムと 比較して、SwiftUIなら ではの特別な動作です。 プロポーザルは下方向だけでなく、 上方向にも流れます。
このレイアウト処理は再帰的です。 プロポーザルはルートビューから ビュー階層全体を下に向かって流れます。
その後、レスポンスが 上に向かって戻ってきます。
サイズ情報が手元にある状態で。 ルートビューは、すべてのサブビューに対して、 どこに描画すべきかを指示します。 サブビューはさらにその下の サブビューに指示し、 すべてのビューが描画されるまで このプロセスが繰り返されます。
それでは、もう一度同じ トラバーサルをご説明いたします。 今回も、こちらの旅行カードに 実際のビューと実際の値を用いて説明します。
旅行カードは、水平スクロールビュー内の 水平スタックに表示されます。
ソース画像のサイズや 旅行名の長さにかかわらず、「Kali」、 「Kyoto」などのすべての旅行カードの 高さを同じにしたいと考えています。
これを実現するには、 固定高さのフレームを使用します。
SwiftUIでは、 VStackに対して幅を無制限にし、 高さを220に固定します。 これは、旅行カードのルート部分での設定です。 その後、VStackは 旅行カードのImageViewに対して、 高さ220のサイズを割り当てます。 こちらが、固定高さフレームの例です。 ImageViewは、 必要な正確なサイズである 185 × 150ポイントで応答します。
外側のスタックは、返された高さを差し引き、 残りのスペースを旅行名を含む テキストに割り当てます。
旅行名のテキストは、 必要なスペースである 15 × 130ポイントのサイズを返します。 これで最後のビューに到達しました。
これで、各ビューが必要なサイズを返しました。
これは、アクティビティオーバーレイを 除く各ビューのことです。 アクティビティオーバーレイについては、 プレゼンテーションの後半で改めて説明します。 処理を完了します。 VStackは、内側の画像、ビュー、 テキストの各リクエストサイズを統合し、 ビューの最終的なリクエストサイズを返します。
これがSwiftUIの レイアウトの仕組みです。
まとめると、SwiftUIのレイアウトは 上から下へと順に処理されます。 そして、サイズ情報は ルートまで逆方向に共有されます。
どのレベルにおいても、利用可能なスペースは コンテナビューによって提供されます。
必要なスペースはサブビューによって決定され、 場合によってはそのサブビューが 自身のサブビューを確認することもあります。
レイアウトの再計算は、 状態が変化した場合にのみ行われます。
ビューテンプレートは常に再作成されます。
状態がコンテンツを制御するため、 トリップ値が変化すると、 ビューは自動的に更新されます。
素晴らしいですね。それでは、魅力的なSwiftUIビューを 作成してみましょう。
私がSwiftUIを始めたばかりの頃、 ビューを作成しても、期待した 結果が得られないことが時々ありました。 そこで次に、レイアウトをシンプルかつ 組み合わせやすいものにすることで、 予測可能な結果を得るためのガイドラインをいくつか ご紹介します。
予測可能なレイアウトを実現するための最初の ポイントは、レイアウト優先度です。
レイアウト優先度は、間隔の調整や サイズ変更の際に、 ビューが配置される 順序を決定するために使用されます。
ビューの要求を満たすのに 十分なスペースがない場合、 レイアウト優先度が高い ビューにより多くのスペースが 割り当てられます。レイアウト優先度は 「layoutPriority」修飾子で 設定され、デフォルト値はゼロです。 値が大きいほど、優先度が高くなります。 例えば、優先度3のビューは、 優先度1のビューよりも優先度が 高くなります。
これは、おそらく皆さんが バグ報告システムで慣れ親しんでいるものとは 逆の仕組みでしょう。 バグ報告では p1 が最優先ですが、 この場合はその逆になります。 数値が大きいほど、優先度が高くなります。
ウィッシュリストアプリでは、 カードの幅が広い「旅行」 セクションを作成しました。
ウィッシュリストアプリのデザイナーである マホさんと話し合ったところ、 この幅広の旅行カードこそ、 余分なスペースを活用して旅行のサブタイトルや 作成日を表示するのに 最適な場所だと考えました。
コンテナビュー内に複数のテキストビューが 含まれている場合、 利用可能なスペースに応じて、 テキストが切り詰められたり、 改行されたりすることがあります。
この例では、サブタイトルと 日付を追加したところ、 カードの左下隅にある「ハワイ、 アメリカ合衆国」というテキストが 切り詰められてしまいました。
利用可能なスペースがどれくらいあるかは、 必ずしも把握できない場合があります。 例えば、デバイスのサイズによって 異なる場合があります。
また、どれくらいのスペースが 必要かも分からない場合があります。 旅行によってはサブタイトルの文字列が 長くなることもあるため、レイアウト内の 各テキストの重要度を指定しておくことで、 どのテキストを切り 捨てるかを判断できるようになります。
これを行うには、 テキストのレイアウト優先度を、 デフォルト値の「0」から「1」 などのより高い値に上げればよいのです。
先ほどお伝えしたように、 スタックは優先度の高いビューに 優先的にスペースを割り当てるため、
サブタイトルには必要なスペースがすべて 確保されますが、 その代わりに右下隅の日付テキストが 切り捨てられてしまいます。
このレイアウトの問題について、 デザイナーのマホさんに相談しました。 彼女は、スペースが足りない場合は作成日を 非表示にし、旅行のサブタイトルが完全に 表示されるようにすることを 提案してくれました。
文字列の切り捨てを回避できるため、 これは素晴らしいアドバイスだと思います。 それでは、その方法をご説明します。
スタックを、サイズが合う ビューの中に移動させました。 サイズが合うビューは、 そのサブビューを評価します。 初期化子に指定した順序に従い、
提案されたサイズ内に理想サイズが 収まる最初のサブビューが選択されます。 つまり、優先順位の高い順に ビューを指定することになります。 通常、この順序は大きいものから 小さいものへと並べます。
そこで、サブタイトルのみを含む テキストビューを追加しました。 ビューが配置されると、スタックは テキストビューを選択するようになります。 これらの変更により、デザイナーのマホさんもきっと 喜んでくれるはずです。
次に、スタックの配置についてですが、 スタックはビューの配置を 揃えるように配置します。 デフォルトの配置は中央揃えですが、 他の配置も指定できます。
中央揃えに加え、 Hスタック内のビューは、 上端や下端に垂直に揃えたり、 テキストの最初のベースラインや最後のベースラインに 揃えたりすることも可能です。
Vスタック内のビューも、 水平方向に先頭端や末尾端などに揃えることが できます。
ウィッシュリストアプリの旅行コレクションの セクションヘッダーは、 その好例です。 デフォルトでは、スタックは ビューを中央揃えにしますが、
要素が連続している場合、 位置がずれて見えることがあります。 このような場合は、すべて ベースライン揃えにするのが最適です。
ここでは、セクションサブタイトルの サブビューが デフォルトの配置を使用しています。 つまり、「すべて表示」が、 サブタイトルのテキストによって確立された 視覚的なラインより上に浮いてしまっています。
配置を「最初のテキストベースライン」 に変更することで、 「すべて表示」をサブタイトルによって 確立されたのと同じ視覚的なラインまで 引き上げることができます。
それでは、SwiftUIで レイアウトを構築する際に考慮すべき点を、 さらにいくつかご紹介いたします。
可能な限り、明示的なレイアウトではなく、 適応型レイアウトを作成してください。 適応型レイアウトを使用することで、 さまざまなデバイスサイズ、ウィンドウサイズ、 プラットフォームへの対応が容易になります。
ビューの高さや幅を明示的に 指定するのではなく、 ビューのフレームを明示的に操作することは 避けてください。 利用可能なスペースを埋めるように ビューを拡張させてください。
frame や position などのビュー修飾子は、 希望するレイアウトを適応的かつ柔軟な方法で 実装できない場合にのみ使用してください。
Zstack と同様です。 ビューに奥行きを加えるには、 background や overlay モディファイアを使用できます。
overlay および backgroundモディファイアは レイアウトのサイズ計算に 含まれず、常に修正対象のビューと 同じサイズになります。
これが、先ほどのレイアウトツリーの トラバーサル例において、 activities の overlay が レイアウトの計算に含まれなかった理由です。 それは、コンテナビューの計算済み サイズを継承していたためです。 デフォルトでは、image view です。
レイアウトの問題は起こり得ます。 私自身も、今でも時々遭遇することがあります。 ここでは、そうしたレイアウトの 問題を特定し、修正するために私が 使用しているテクニックをいくつか ご紹介いたします。
その多くは、迅速なプロトタイピングを可能にする SwiftUIの力を活かしたものです。 わずかな調整を加えるだけで、 結果が即座に反映されます。
簡単なテクニックの一つとして、 こちらの「trip」 カードで行ったように、画像には赤、 テキストには青といった色の境界線を 追加する方法があります。
この手法は、スタックレイアウトや ビュー周辺のパディングを理解するのに特に 役立ちます。
一時的な境界線が重なってしまうこともあり、 サブビューのサイズや位置をより 明確に表現したい場合があります。 そのような場合は、半透明の色を使った オーバーレイ修飾子を使用して、 サブビューの全範囲や重ね 合わせ方を可視化します。
また、ビューがレンダリングされる際に プロパティの値を出力するのも有効です。
printAnd を使用しようとして、 コンパイラエラーが出てがっかりした 経験があるかもしれません。
ここでは、「buildExpression は 利用できません。 この式は View に準拠していません」 というエラーが表示されます。 このエラーは、Swift のprint式には 戻り値がないため、その戻り値の型が
void であることに起因しています。 void は View ではありませんが、View の本体では 変数の宣言をサポートしています。
プレースホルダー変数を宣言することで、 このエラーを修正できます。 これで、等号の右側に print 式を記述できるようになりました。 これでエラーなくコンパイルされ、 実行時にコンソールに出力が表示されます。
次に、Xcode には プレビューキャンバスがあり、 どのビューがどのコードに 対応しているかを素早く確認できます。 キャンバスの下部にある マウスポインタのアイコンをクリックすると、 プレビューが選択可能になります。
そうすると、エディタでコードを選択すると、 プレビューで関連する ビューがハイライト表示され、 プレビューでビューを選択すると、 Xcodeがエディタで 対応するコードを選択してくれます。
レイアウトの問題をデバッグするための、 私のツールボックスの中で最も強力なツールは、 Xcodeの「ビューデバッガー」です。 アプリを実行し、ビューデバッガーで 停止させることで、 すべてのビューを展開し、 個々のサブビューを選択して 詳細を確認することができます。 この手法は、SwiftUIとUIKitを 併用している場合に特に効果的です。 ここで重要なポイントがあります。
SwiftUIはUIKitの レイアウトと互換性があります。 AlltrailsのJames Graham氏が 後ほど詳しく説明してくれます。 詳細については、WWDC 22の 動画をご覧ください。 SwiftUIとUIKitを併用しましょう。
SwiftUIにおけるレイアウトの 基本についてご紹介しました ので、次はこれらの概念を実践してみましょう。
学ぶには、実際に作り始めるのが一番です。 私はまず、気に入ったアプリから ビューを1つ選び、 それをSwiftUIで 再現することから始めました。
Xcodeのプレビュー機能を使えば、 さまざまなレイアウトやアプローチを素早く 試すことができます。
printやカラーオーバーレイといった シンプルなテクニックを活用して、 レイアウト上の問題を特定しましょう。 問題の特定と修正が驚くほど迅速に 行えることに、きっと驚かれることでしょう。
本日はご参加いただき、 ありがとうございました。 それでは、SwiftUIを使って 何か特別なものを制作し、 残りのセッションも存分にお楽しみください。
-