ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIによるカスタムレイアウトの作成
SwiftUIでは、レイアウトのレベルを上げたり、Appのインターフェイスのビューを配置する強力なツールが利用できます。ここでは、Gridコンテナについて解説します。これにより、高度なカスタマイズが可能な2次元レイアウトが作成できるようになります。さらに、Layoutプロトコルを使用して、完全なカスタム動作を有する独自のコンテナを構築する方法についても解説します。また、レイアウトタイプ間でシームレスなアニメーション効果を作り出す方法や、優れたインターフェイスを作成するためのヒントやベストプラクティスも紹介します。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC20
WWDC19
-
ダウンロード
♪ ♪
「SwiftUIによるカスタムレイアウトの作成」 へようこそ 私はPaulです Developer Documentationで働いています SwiftUIはAppのインターフェースを構成する 豊富な構成要素のセットを提供します テキスト 画像 グラフィック などの組み込みViewを 組み合わせ カスタムや複合Viewを作成できます これら要素を洗練された グループに配置するため SwiftUIはレイアウトツールを提供しています
水平と垂直スタックのような コンテナは Viewを互いに 相対的に配置する場所を SwiftUIに伝えることができ Viewモディファイアは間隔や配置などを さらに制御することができます この講演では一般的なレイアウトを より簡単に構築しより 複雑なレイアウトを可能にする 新しいツールを紹介します 途中 SwiftUIでレイアウトを 操作するヒントを提供します まず 静的なViewのセットを表示する場合に 2次元のレイアウトに 最適なグリッドファミリーの 新しいメンバーを紹介します 次に 新しいレイアウトプロトコルを使用して レイアウトエンジンと直接対話できる カスタムViewコンテナタイプ の作成方法を説明します さて ViewThatFitsについて説明します Viewのコレクションから 利用可能なスペースに一致する Viewを自動的に選択するコンテナタイプです 最後にAnyLayoutを使用して レイアウトタイプ間の シームレスな移行を追加する方法を紹介します これらの新機能を使ってみるため 私が手掛けているAppを見てみましょう
ここ数年 私の仲間内で最高の毛皮の ペットになるのは誰かという 論議が交わされています 私自信の意見もありますが 何か一致した意見がないか 投票を行うAppを作ることにしました そして毛皮アレルギーの人々も考慮して 選択肢を1つ増やしました SwiftUIでインターフェースの デザインを行うのが好きです プロトタイプの作成が プレビュー使用で簡単です その出発点として簡単なスケッチを描きました 投票が一定期間続くと想定して 順位を示すリーダーボードを 真ん中に置きたいです 一番下に投票用のボタンを設置します トップに投票された内容を画像で表示します
まず最初にリーダーボードを 作りたいと思います では それを詳しく見ていきましょう リーダーボードは2次元のグリッド構成で 候補者ごとの行と 名前 得票率 得票数の列が表示されます ここで実現したいことが 具体的にいくつかあります まず 得票率を表すprogress viewsは できるだけ大きなスペースを確保したいので 2つのテキスト列はそれぞれのケースで 最も広いセルを収容するのに 必要な幅だけにしています そしてこれは どんなに文字数が増えても 他の言語を話す友人や異なる文字サイズの デバイスを使う人にとっても 同じである必要があります 次に名前は前揃えにしたいです しかし合計は後揃えにしたい Lazy gridsはスクロール可能な コンテンツに最適です コンテナは多くのViewを 持つ場合に非常に効率的です 表示中か表示されようとするViewだけを 読み込むからです 一方 コンテナは自動的にセルのサイズを 両軸で調整することはできません
例えば LazyHGridは列を描画する前に 列内のすべてのViewを測定できるため 各列の幅の算定ができますが 行の高さの算定のために 全てのViewの測定はできません この機能を実現するために Lazy Gridは初期化時に その次元の1つについて 情報提供する必要があります
Lazy gridsと SwiftUI layout container typesは 2020の「SwiftUIのStack Grid Outline」 をご覧ください 私の場合 スクロールの必要はありませんし SwiftUIに各セルの高さと幅 の両方を計算させたいのです このレイアウトのためSwiftUIは Grid Viewを提供します Lazy gridとは異なりグリッドは すべてのViewを一度にロードするため 列と行の両方にわたって 自動的にセルのサイズと位置を 調整することができます そのためのコードを見てみましょう これはGridとしたリーダーボードの 基本バージョンです GridViewには3つの GridRowインスタンスがあります 1つの行の中で各Viewは列に対応しています この例では 各行の最初のテキストViewが 最初の列に対応し 進行状況Viewは2番めの列にあります そして最後のテキストViewは3列目にあります Gridはその最大のViewを 保持するために必要なスペースを 各行と列に割り当てていることに 注意してください 最初のテキスト列には長い名前に 十分な幅があります 進捗状況インジケーターのような 柔軟なViewはGridが 提供するスペースと 同じだけのスペースを取ります スペースはテキスト列を 確保した後に残ったものです 少し調整したいのですが まず基本的なデータモデルを 作成して 投票数を保存できるようにします
ネットワーク上のデータを 管理・共有するためロジックが必要ですが プロトタイプを作成中は単純な構造が必要です この型を ForEachで使い安くするために 識別可能なコンフォーマンスを含め Equatableコンフォーマンスに準拠し アニメーションを変化可能にします
次に プロトタイプを作成し サンプルデータを作ります Gridに戻りステート変数を作成し サンプルデータで初期化することができます そのデータを使ってForEachで 行を作ることができます レンダリング出力が変化していないのは 同じデータが表示されているからです すでにかなり近いですがセルの調整が必要です 現在すべてのセルは中央揃えされており これはGridのデフォルトです しかし 覚えている限りでは 名前は前揃えに 値は後揃えにしたいのです そのためにGridを前揃えで初期化します 使用する値は Grid内の すべてのセルに適用されます 最初の2つの列は上手くいきましたが 最後の列は? 1つの列の調整にはその列の任意の1つのセルに gridColumnAlignmentの ビュー修飾子を適用できます そこで 最後の列にある テキストViewでやってみます なるほど そこまではいいのですが 各列の間に 仕切りがあったほうがいいような気がします 新しい行を仕切り付きの For Eachに追加するだけなら 私が期待するものではないのですが これには2つの興味深いことがあります まず 仕切りが柔軟なViewであるため 1列目のスペースが広くなってしまします 基本的にGridは最後の列に必要なものをいれ 残りのスペースを最初の 2つの列に分割しています 次に 他のGrid行に比べて Viewの数が少ないGrid行は missing Viewは後の列で 空のセルを作成するだけです 私の望みは全ての列をまたぐ 仕切りでSwiftUIには それを行う新しいViewモデファイアがあります
Gridセル列モディファイア をViewに追加することで 1つのViewがいくつかの 列にまたがるようにできます この場合 3つの列にまたがって表示されます ViewがGrid全体をカバーする場合は Gridの行の 外側にViewを単独で書くことで簡略化できます さてリーダーボードがよい状態になったので 次に投票ボタンを見てみましょう
一見すると派手さはありません しかし ひとつだけ特別な要求があります 一方では 特定の選択肢のボタンを小さくして 参加者を偏らせたくはありません だた ボタンがコンテナと 同じ大きさも避けたいです iPadやMacでは非常に 大きくなる可能性があります ボタンは最も幅の広いボタン テキストと同じにします ではこれをHstackで作ると どうなるのでしょう? 各ボタンはテキストラベル に合うようにサイズ調整され HStackはこれらを水平方向にまとまています このデフォルトのスタック 動作は多くは望まれますが このプロジェクトの私の 使用には全く合っていません
SwiftUIのレイアウトの基本はこちらを 2019「SwiftUIでカスタムビューを構築する」 その時のコンセプトを使って このView階層をみて 何を変更すれば望む動きに なるのかを見てみましょう
まずスタックのコンテナは大きさを提案します これを元に スタックは3つの ボタンにサイズを提案し 各ボタンはそのサイズを テキストラベルに渡します テキストViewは含まれる文字列によって 実際に欲しいサイズを計算し これをボタンに報告します ボタンが情報を受け渡します スタックはこの情報をもとに 自分のサイズを決め ボタンを配置しコンテナに 自分のサイズを報告します ボタンがテキストの大きさに合わせて表示され テキストViewをフレームで囲み 拡大できたらどうでしょう? テキストは変更なしですが ボタンは柔軟なサブViewを 参照し HStackが提供する 同じスペースが使用されます スタックはそのスペースを 含むViewに均等に分割します そのため ボタンが同じサイズなりましたが 実際のサイズはスタックの コンテナに依存します スタックはコンテナが提供 するあらゆるスペースを 埋めるために拡張されますが 私が望むものではありません 私が欲しいのは各ボタンの 理想的なサイズを尋ね 最も幅の広いものを見つけ そのマウントを提供する カスタムスタックタイプです 幸いにもSwiftUIにはそれを 行う新しいツールがあります レイアウトプロトコルを 使用すると 私の使用事例に 合ったレイアウト処理に直接参加する カスタムレイアウトコンテナ を定義することができます その様子をご覧ください 特定の問題を解決するためHStackをもう一度見て これをEqualWidthHStackに変更してみましょう このタイプはボタンに等しく幅を割り当てます ボタンの理想的な幅と同程度の幅です 柔軟なフレームを使用し 幅の狭いテキストのボタンが スタック内のスペースに合わせて拡張できます しかしボタンには 私が測定できる理想的な サイズがありますテキストの幅です Equal Width HStackの実装を見てみましょう
まず レイアウトプロトコル に準拠した型を作成します 基本的なレイアウトに必要 なのは 2つのメソッドです そのためスタブを追加しましょう 最初のメゾットはsizeThatFitsで レイアウトコンテナの 大きさを計算し 報告します
私のレイアウト自身のコンテナViewからの サイズ提案であるViewサイズの入力ができます subviewsパラメーターで サブViewにサイズを提案できます
サブViewに直接アクセス できないことに注意です その代わり サブViewの入力は サイズの提案など サブViewと 特定の方法で対話できる プロキシのコレクションで サイズを提案するようなものです 各プロキシは提案された 具体的なサイズを返します その回答をすべて集めて計算を行い 同じ幅の HStackの具体的な大きさを そのコンテナに返します
2つ目のメソッドとして placeSubviewsを実装します レイアウトのサブViewの表示位置を指定します これは同じサイズの提案と サブViewの入力を受け取り さらに範囲入力を受け取り それはサブViewを配置する 必要のある領域を表します Boundsは私が求めたサイズを 実装に合わせた長方形です いいですか ViewはSwiftUIでサイズを選びます 私のレイアウトコンテナは 求めたサイズを取得します 領域の原点は左上です 正のXが右側で正のYが下方にあります 右から左への言語環境であっても すべての配置計算を想定することができます その方向にViewをレイアウト するとき フレームワークが 自動的に各ViewのX位置を反転させるからです ただし長方形の原点が(0,0)だと 仮定しないでください 特に原点が0でない場合レイアウトのサブViewを 配置するメソッドが別のレイアウトの 同じメソッドを呼び出すような レイアウト合成を可能にします 作業し易いように 長方形には 各次元の最小点 中心点最大点など 領域の重要な部分にアクセスするための プロパティが用意されています
次に進む前に これらのメソッドが持っている もう1つのパラメーターに注目してください これはメゾットコールを またいで 中間計算の結果を 共有するため使用する双方向キャッシュです 多くのシンプルなレイアウト では これはい必要ないです 私もキャッシュを無視することにします しかし Instrumentsを使用し Appをプロファイリングし レイアウトコードの効率を 改善する必要がある場合 Instrumentsの追加を検討してください ドキュメントをチェックするをご覧ください
では sizeThatFitsを実装してみましょう 水平に配置されたすべてのボタンが 同じ幅で収まるようコンテナ のサイズを返したいのです まず 各ボタンに提案したサイズを要求して 何か返ってくるか見てみましょう サブViewの柔軟性を測るために 複数の最小 最大 理想的な サイズ特別な提案を使用して 複数の測定を行うことができ または特定のサイズを提案することができます この場合 特定されていない サイズの提案を使います
そして戻ってきたすべてのサイズについて 各寸法で最大の値を求めます この場合金魚ボタンが幅を設定し 高さはすべて同じになります さて これをメソッドに リファクタリングしましょう サブViewを配置するときに 再び必要になるからです 次にViewの間隔を計算する必要があります 10ポイントなど一定の間隔にすればいいのですが レイアウトプロトコル使用で もっとよい方法があります SwiftUIではすべてのViewは それ自身と次のViewの間に あるスペースの量を示す間隔設定を持ちます これらの設定はViewSpacing インスタンスで保存され レイアウトコンテナで使用できます Viewは異なるエッジで 異なる値を選ぶ場合があり また隣接するViewの種類に よって異なる値もあります Viewはそれ自身とテキスト Viewの間に 画像とは多少 差があるスペースを必要とするかもしれません プラットホームによってもその値は異なります これらの設定を無視することもできます これはカスタムページングで 組み込みスタックを 初期化したときに起こることです あなた自身のレイアウトで 環境設定を尊重することは 自動的にApple’s interface guidelines に従う結果を得るための良い方法であり 結果 システムの他の部分の外観と一致します さて どのViewもすべてのエッジに 環境設定を持っていますが 2つのViewを一緒にすると 共通のエッジの環境設定が 一致しないことがあります この解決のため組み込みレイアウトコンテナは 2つの環境設定のうち大きい方を使用します 同じことを私のレイアウト で行うことができます
サブViewのプロキシは与えられた軸に沿って 他のボタンに各ボタンの良い 間隔の求め方を提供します そこで値の配列を作成してみましょう サブViewを走査し各プロキシの spacingインスタンスで 間隔法を呼び出し水平軸に沿った次のViewの spacingインスタンスまでの間隔を取得します この呼びかけは共通のエッジ で両方のViewの参照を考慮します この配列の最初の要素は 猫ボタンが金魚ボタンに 対して水平方向にどれだけの スペースが必要か 次に金魚の ボタンが犬のボタンに対して どの程度必要か示しています 配列の最後の要素は比較するボタンがないので 強制的にゼロにします さて これをメソッドに リファクタリングしましょう これで間隔値を組み合わせて 全体の間隔の合計を求め それを幅と高さの測定値を合わせて サイズ値を返すことができます サブViewの理想的なサイズと 各サブViewの好ましい間隔を考えると これは私のレイアウトに必要なサイズです もう1つ必要なメソッドはplaceSubviewsです 私はコンテナの境界とボタン を指示するため使用できる サブViewのプロキシの コレクションを取得します まずsizeThatFitsメソッドで 行ったのと同じように maxSizeとspacin配列を計算します ここでもこれらの値が必要になるからです 次に各サブViewに使用できる サイズ案を作成します 今回は理想的なサイズでなく 持ってほしいサイズに基づいて作成します ボタンを同じにしたいので 1つの案だけでいいのです そして境界線の先端と ボタンの幅半分を加えたもの として計算された最初のサブViewの水平方向の 開始位置が決まります 原点がゼロであることに 依存しないことに注目です 代わりにminXの値から始めています 最後に 各サブViewのプロキシを 通過して 点 その点がボタン から何を示すのかのステートメントおよび サイズの提案でその配置メソッドを 呼び出すことができます ループのたびに Viewの幅と 次のViewのペアの間隔を加えて 水平位置を更新し 次の反復に備えます これで終わりです 新しいViewレイアウトタイプ を使うとどうなるでしょう
すると こうです 組み込みのHStackと同じように 独自のレイアウトコンテナを インスタンス化すると ボタンが水平にすべて同じ幅に配置されます ここで少し立ち止まってLayoutプロトコルが 過去にGeometry readerを 使用したかもしれない問題を どのように解決したかをお話します Geometry readerはView サイズを測定するツールです しかし この場合はベストな選択とは言えません Geometry readerはコンテナ Viewを測定し そのサイズを サブViewに報告するよう 設計されているからです サブViewはその情報をもとに 独自のコンテンツを描きます Geometry readerの使用目的では 情報が下に流れることに注意してください リーダーが行う測定はそれ自身のコンテナの レイアウトに影響を及ぼしません
これはコンテナに合わせて スケールを描くのに最適です Geometry readerは有効な スペースをパスロジックに 知らせ サブView内の パスロジックが調整されます コンテナのサイズが変わればパスも変わります Geometry readerは新しいサイズを渡すからです 私のボタンの場合 見やすい ように1つだけ取り上げます テキストViewを測定し コンテナであるフレームを どのように設定するかを 決定する必要があります そこでGeometry readerをテキストViewの オーバーレイに追加しコンテナを測定して 通常の流れとは別に 測定データをフレームに送信します この実行は レイアウトエンジンの回避となり ループが発生する可能性が あることに注意してください リーダーはレイアウトを 測定し フレーム変更します レイアウトが変更され再度 測定が必要な場合があります 現在これを動作させることは 可能ですが 注意しないと Appをクラッシュさせてしまう可能性があります その結果この戦略は推奨されません 幸い レイアウトプロトコル では レイアウトエンジン内 で作業することで この問題解決 のより良い方法を提供します ではもう一度ボタンを見てみましょう ここにもやりたいことがあります まず少し読みやすくするために ボタンを独自のサブViewに リファクタリングします 今 私の同僚の1人がより大きな文字を 使用していることを知っています 私のAppはデフォルトの フォントを使用して自動的に Dynamic Typeをサポートし 無料で正しい動作が得られます タイプサイズを大きくすると どうなるか見てみましょう あーボタンが合わなくなってしまいました 私のカスタムスタックはボタンの幅を制限せず 理想的なサイズにすることを 忘れないでください この場合 ディスプレイの幅を超えています では どうすればいいのでしょうか? コンテナから提案されるサイズを考慮して Viewが収まらないときにレイアウトを修正して もっと複雑なことをすることもできます 今回のケースは 新しいview-that-fitsコンテナ を使い ほとんどの作業を やってもらうことができます この新しいタイプは 私が 与えたViewのリストから 使用可能なスペースに収まる 最初のViewを選びます
カスタムスタックをview-that-fits構造で包み 垂直スタックバージョンを追加することで SwiftUIにボタンの別の配置 の必要性を把握させます もちろん 組み込みVStackに カスタム水平スタック のような等幅プロパティはありません そこで カスタムスタックの 垂直版も実装してみました すでに説明したものとよく似ていますね だたし同じ幅のアイテムを 横軸でなく縦軸に配置することです
そしてもちろん 動的なタイプ サイズのオーバーライドを 解除すると 水平方向のレイアウトに戻ります 最後にもう1つAppを構築する必要があります それは 上部にある画像です プロフィールの写真をまとめて表示するような 簡単なものならできますが 少し楽しんでみようかと そこでViewを円形に配置し その配置を順位に応じて回転させる カスタムレイアウトタイプをもう1つ作りました この構成だと金魚が1位です そして 他の2つは同点2位です 犬が猫を引き離した場合 少し回転させて表示できます あるいは 放射状配置を回転させることで より現実的な結果を表示することもできます レイアウトプロトコルで簡単に作成できます 先程と同じように 2つの メソッドが必要なだけです 適切なサイズでは Viewを 使用可能なスペースにいれ コンテナViewが提案するサイズを返します replacing-unspecified- dimensionsメソッドで 具体的なサイズに変換して見ることにします そのメソッドは コンテナが 理想的なサイズを要求すると 存在し得るnil値を自動的に処理します そして サブViewを配置するメソッドの内部で レイアウト領域のサイズに基づいた半径で 中央から各サブViewをオフセットし Viewの Viewのインデックスに 依存する回転を適用します 基準値としてこれは Viewを円の 3分の0 1 2分の1に配置するものです 現在のランキングを反映させるため すべてのViewに等しく影響 するオフセットを適用します しかしランキングは どこで 手に入れるのでしょうか レイアウトのアクセスは サブViewのプロキシのみで Viewやデータモデルにはアクセスできません さて レイアウトには もう1つトリックがあります 各サブViewに値を保存しその値をレイアウト プロトコルのメソッド内部 から読み取ることができます どのように 順位情報を伝えるか見てみましょう まず レイアウトバリュー キープロトコル基準した 新しい型を宣言し それに デフォルト値を与えます 明確に設定しない場合Viewの値の提供に加え デフォルトの値は関連する値の型 この場合は整数を確立します そして ViewにlayoutValue view modifier を使用し 値を設定する 便利なメソッドを作成します これで View階層でレイアウト内のViewに 便利なランクモディファイアを 適用することができます ここで 各ペットのランクを 計算し 放射状配置に そのペットに対応する アバターViewに追加します 最後にサブViewを配置するメソッドに戻ります レイアウト値のキーを インデックスとして使用し 各サブViewから値を読み取る コードを追加できます そのランクを利用して オフセットを計算できます そのロジックは割愛しますが基本的に あらゆる ランキングのセットに対して 適切な角度を作り出します まあ 1つを除いては 3つ揃ったらどうなるのでしょう? レイアウトを回転させ一列に Viewを並べる方法はないので 別の レイアウトロジックで 代用するしかありません しかし これを実現する レイアウトタイプはすでに 存在します組み込みのHStackです 私が望むのは3者同点となった時に HStackに移行することです そのための新しいツール があることもわかりました あるレイアウトタイプの 使用は 1つのView階層に 異なるレイアウトを適用することができます レイアウトの種類を変更は Viewの同一性を維持します ここでは前に見た放射状配置がありますが これを三者同点に依存する 新しいレイアウトタイプに 置き換えるだけです ThreeWayTieプロパティは 状態から発生したもので 変更されるとSwiftUIはそれに気づき このViewを再描画する 必要があることを認識します View階層の構造的な同一性は常に同じままなので SwiftUIはこれを新しいViewとしてでなく 変更されたViewと見なします その結果1行加えるだけで レイアウトの切り替えがスムーズに行えます 実際に animation view モディファイアを追加すると 放射状配置の異なる状態の アニメーションも得られます 放射状配置の構成が同じ データに依存するからです そしてこれがすべての動作の様子です ボタンをタップして投票数を 変更すると アバターが スムーズに動き 現在の順位が反映されています
これらは AppのViewレイアウトを構成する SwiftUIの新しいツールの一部です Gridタイプを利用するとカスタマイズ性の高い 静的情報の二次元レイアウトを 構築することができます レイアウトプロトコルの 使用で 汎用的で再利用可能 または特殊な例に特化した レイアウトを定義できます 利用可能なスペースに最も フィットするようにSwiftUIに 選ばせたい場合ViewThatFitsを使用できます AnyLayoutは レイアウト間を シームレスに移行できます ありがとうございました 新しいレイアウトツール
-
-
4:28 - Grid with explicit rows
struct Leaderboard: View { var body: some View { Grid { GridRow { Text("Cat") ProgressView(value: 0.5) Text("25") } GridRow { Text("Goldfish") ProgressView(value: 0.2) Text("9") } GridRow { Text("Dog") ProgressView(value: 0.3) Text("16") } } } }
-
5:16 - Data model
struct Pet: Identifiable, Equatable { let type: String var votes: Int = 0 var id: String { type } static var exampleData: [Pet] = [ Pet(type: "Cat", votes: 25), Pet(type: "Goldfish", votes: 9), Pet(type: "Dog", votes: 16) ] }
-
5:41 - Final Leaderboard
struct Leaderboard: View { var pets: [Pet] var totalVotes: Int var body: some View { Grid(alignment: .leading) { ForEach(pets) { pet in GridRow { Text(pet.type) ProgressView( value: Double(pet.votes), total: Double(totalVotes)) Text("\(pet.votes)") .gridColumnAlignment(.trailing) } Divider() } } .padding() } }
-
10:53 - Layout protocol stubs for required methods
struct MyEqualWidthHStack: Layout { func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Return a size. } func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { // Place child views. } }
-
13:44 - Maximum size helper method
private func maxSize(subviews: Subviews) -> CGSize { let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in CGSize( width: max(currentMax.width, subviewSize.width), height: max(currentMax.height, subviewSize.height)) } return maxSize }
-
15:40 - Spacing helper method
private func spacing(subviews: Subviews) -> [CGFloat] { subviews.indices.map { index in guard index < subviews.count - 1 else { return 0 } return subviews[index].spacing.distance( to: subviews[index + 1].spacing, along: .horizontal) } }
-
16:33 - Size that fits implementation
func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Return a size. guard !subviews.isEmpty else { return .zero } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let totalSpacing = spacing.reduce(0) { $0 + $1 } return CGSize( width: maxSize.width * CGFloat(subviews.count) + totalSpacing, height: maxSize.height) }
-
16:51 - Place subviews implementation
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { // Place child views. guard !subviews.isEmpty else { return } let maxSize = maxSize(subviews: subviews) let spacing = spacing(subviews: subviews) let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height) var x = bounds.minX + maxSize.width / 2 for index in subviews.indices { subviews[index].place( at: CGPoint(x: x, y: bounds.midY), anchor: .center, proposal: placementProposal) x += maxSize.width + spacing[index] } }
-
18:07 - Custom layout instantiation
MyEqualWidthHStack { ForEach($pets) { $pet in Button { pet.votes += 1 } label: { Text(pet.type) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } }
-
20:12 - Buttons helper view
struct Buttons: View { @Binding var pets: [Pet] var body: some View { ForEach($pets) { $pet in Button { pet.votes += 1 } label: { Text(pet.type) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } } }
-
21:08 - Final voting buttons view
struct StackedButtons: View { @Binding var pets: [Pet] var body: some View { ViewThatFits { MyEqualWidthHStack { Buttons(pets: $pets) } MyEqualWidthVStack { Buttons(pets: $pets) } } } }
-
22:30 - Radial size that fits
func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { // Take whatever space is offered. return proposal.replacingUnspecifiedDimensions() }
-
22:52 - Radial place subviews without offsets
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { let radius = min(bounds.size.width, bounds.size.height) / 3.0 let angle = Angle.degrees(360.0 / Double(subviews.count)).radians let offset = 0 // This depends on rank... for (index, subview) in subviews.enumerated() { var point = CGPoint(x: 0, y: -radius) .applying(CGAffineTransform( rotationAngle: angle * Double(index) + offset)) point.x += bounds.midX point.y += bounds.midY subview.place(at: point, anchor: .center, proposal: .unspecified) } }
-
23:42 - Rank value
private struct Rank: LayoutValueKey { static let defaultValue: Int = 1 } extension View { func rank(_ value: Int) -> some View { layoutValue(key: Rank.self, value: value) } }
-
24:21 - Radial place subviews with offsets
func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { let radius = min(bounds.size.width, bounds.size.height) / 3.0 let angle = Angle.degrees(360.0 / Double(subviews.count)).radians let ranks = subviews.map { subview in subview[Rank.self] } let offset = getOffset(ranks) for (index, subview) in subviews.enumerated() { var point = CGPoint(x: 0, y: -radius) .applying(CGAffineTransform( rotationAngle: angle * Double(index) + offset)) point.x += bounds.midX point.y += bounds.midY subview.place(at: point, anchor: .center, proposal: .unspecified) } }
-
25:18 - Final profile view
struct Profile: View { var pets: [Pet] var isThreeWayTie: Bool var body: some View { let layout = isThreeWayTie ? AnyLayout(HStackLayout()) : AnyLayout(MyRadialLayout()) Podium() // Creates the background that shows ranks. .overlay(alignment: .top) { layout { ForEach(pets) { pet in Avatar(pet: pet) .rank(rank(pet)) } } .animation(.default, value: pets) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。