View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

その他のビデオ

ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。

  • 概要
  • トランスクリプト
  • コード
  • SwiftUIの最新情報

    SwiftUIのパーティーです。みなさんも招待されています。SwiftUIフレームワーク設計の将来に関する最新情報を紹介しますので、是非ご覧ください。SwiftUIでAppを徹底的に設計する深いレベルでのカスタマイズ、高度なレイアウトテクニック、共有のための洗練された戦略、強固な構造的アプローチをご覧ください。また、最新のグラフィックエフェクトを使用してAPIを解説しますので、楽しみながらご覧ください。

    リソース

    • SwiftUI
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC22

    • カスタムコラボレーションAppを メッセージAppと統合する
    • メッセージAppで共同制作の体験を強化する
    • Apple Watch用の仕事効率化Appの構築
    • iPad Appデザインの最新情報
    • iPadのSwiftUI:インターフェイスをオーガナイズする
    • iPadのSwiftUI:ツールバーやタイトルなどを追加する
    • Swift Charts: より高いレベルへ
    • Swift Chartsの紹介
    • SwiftUI Appに複数のウインドウを追加する
    • SwiftUIによるカスタムレイアウトの作成
    • SwiftUIのナビゲーション機能
    • Transferableの紹介
    • WWDC22(1日目)のまとめ
    • XcodeでマルチプラットフォームAppを開発する
    • Xcodeの最新情報
  • ダウンロード

    ♪メロウなヒップホップ ♪ ♪ Nick です Franck です SwiftUIのエンジニアです 今日はSwiftUIの新機能についてお話しします SwiftUIはOSと一緒に成長しています 正に相互作用です 皆さんがSwiftUIで作り出す 皆さんの作品に感動しています 沢山の意見に耳を傾けました だからこの1年取り組んだ内容を紹介するのが とても楽しみです 今年のAPIはより深く取り組みました 更にカスタムエクスペリエンスを追加しました 既にグラフィック関連の技術をご紹介しました 今回新しいSwiftUIのアプリケーションの 構造などを設計しました SwiftUIで私たちはプラットフォームの未来を デザインする事が可能になりました クラシックなデザインを一気に刷新し システムの深いレベルまで刷新しました Apple内での包括的な採用によって SwiftUIの進化が進みます 新しいデザインと機能の多くが SwiftUIの進化が可能にし AppleのAppの書き方を変えました 今日はこのAPIを称えます そしてSwiftUIの誕生日も祝います Frankと僕は誕生日パーティの 担当です このパーティについて お話ししましょう 新しいフレームワークを導入しました Swift Chartsと呼びます 全プラットフォームでデータを可視化します SwiftUIのデータ駆動型で強い型付けの ナビゲーションモデルと 新しいウィンドウテクノロジーを紹介します Frankが新しいコントローラーを紹介します そして既存コントローラーのカスタマイズも そしてTransferableプロトコルによって SwiftUIの世界にどのようにエレガントに 「共有」を持ち込んだかをお見せします 最後に私が新しいグラフィックAPIと そして新しいアドバンスレイアウトAPI も紹介します ますはSwift Chartsから Swift Chartsは美しいステートドリブンな チャートを構築するための 宣言型フレームワークです SwiftUIとデータプロセスを高品位にする デザインの核にあるものが Swift Charts生成の為に作られた 最高のデータ可視化フレームワークです このチャートはFranckと私が パーティーの開始前に 完了しなければならないタスクの数を プロットした棒グラフです Swift Chartsはわずか数行のコードで カスタマイズ可能な チャートを簡単に作成可能です SwiftUI同様にSwift Chartsも 最適な初期設定を選択します フレームワークはY軸に適した数字を 自動的に選択し そしてバーにも適した色を選択しました

    すでにSwiftUIを知っていれば Swift Chartsの宣言的でステートドリブンな 構文を読み取ることができます データを提供し そのデータでチャートのコンテンツを 構築することによって リストやテーブルで行うのと同じように それを宣言します このチャートではBarMarkを選択しましたが LineMarkに切り替えて カテゴリでグループ化するために 前景スタイルを追加すると Swift Chartsがそれぞれのカテゴリに対して 個別の線を描き チャートに自動的に凡例を追加するので ストーリーがもっと見えてきます チャートに個性を加えるのは楽しいことです LineMarkのシンボル修飾子で 線にポイントを追加することができます これはSwiftUIの修飾子と同様です チャートの中で SwiftUIのビューを使うこともできます リストのようなChartへのデータ引数は 代わりにForEachで渡すことができます これにより毎日の目標を示すRuleMarkのように より多くのマークを Chartビルダーに追加することができます

    SwiftUIの精神はSwift Chartsにも適用され Swift Chartsはローカライズ・ダークモード ダイナミックタイプを自動的に処理し すべてのプラットフォームで動作します あなた自身のチャートを作る方法は 「Swift Chartsの紹介」を参照してください 高度なプロットテクニックに興味があるなら 「Swift Charts: より高いレベルへ」 をご覧ください 次はナビゲーションとウィンドウについてです SwiftUIは既に プッシュ&ポップ式のナビゲーションスタック スプリットビュー マルチウィンドウなど 一般的なナビゲーションパターンを 広くサポートしています

    今年SwiftUIはこれらの3つのパターン すべてに対して 大きなアップデートを行いました まずはStacksから始めましょう SwiftUIはプッシュ&ポップスタイルの ナビゲーションをサポートするために NavigationStackと呼ばれる 新しいコンテナビューを導入します NavigationStackは このパーティ用フードリストのような ルートコンテナビューを作成します NavigationLinkやnavigationTitle()の様な 既存のAPIとも連携します リンクを選択すると SwiftUIはその詳細のビューを スタックの一番上にプッシュします それぞれの詳細ビューには 素早くブラウジングするために 関連する食品アイテムのための より多くのリンクが含まれています

    このアプローチはあなたが必要とする すべてかもしれません しかしビューを表示しその表示状態を プログラムで制御する新しい方法があります ナビゲーションスタックの状態を 制御する必要がある場合は 新しいデータ駆動型APIを採用してください 新しいnavigationDestination()修飾子により ナビゲーション先を 特定のデータ型と関連付けることができます

    今年NavigationLinkに 新しいトリックを組み込みました デスティネーションビューの代わりに デスティネーションを表す バリューによる指定を提供します リンクをクリックするとSwiftUIは そのバリュー型を使って 正しいデスティネーションを見つけ 以前のようにスタックにそれをプッシュします スタックを駆動するためにデータを使用するので 明示的なステートとして 現在のナビゲーションパスを表現することができます この場合ナビゲーションパスは これまでに訪れたすべての食品の配列になります このステートに直接アクセスできるので 最初に選択したアイテムに すばやくジャンプして 戻るためのボタンを追加するのは これ以上ないほど簡単です ビューがスタックにプッシュされると selectedFoodItems 配列に アイテムが追加されます ボタンのアクションでは 最初のアイテムを除き パスから全て削除します

    タップで最初の地点へ戻ることができます

    次にマルチコラムナビゲーションのための スプリットビューについて説明します マルチカラムナビゲーション用に NavigationSplitView という 別の新しいコンテナを導入します NavigationSplitViewは 2カラムと3カラムの レイアウトを宣言することができます Party Plannerではシンプルな 2カラムのレイアウトを使用し パーティープランニングの タスクのサイドバーリストと 選択したタスクによって内容が変わる 詳細ビューをラップしています スプリットビューは先に紹介した新しい 値ベースのNavigationLinksと相性がよく リンクの値を使ってリストの選択を 行うことができます NavigationSplitViewは 小さいサイズクラスやデバイスでは 自動的にスタックに折り畳まれるので アダプティブなマルチプラットフォーム アプリケーションを構築するための 素晴らしいツールになります NavigationSplitViewとNavigationStackは 一緒に動作するように設計されており 直接組み合わせて より複雑なナビゲーション構造を 構築することができます Party Planner Appこれを使用して 詳細列を独自の自己完結型の ナビゲーションスタックに変換しています そして新たにMacOS用の ナビゲーションにも対応しています

    フードについて沢山話しました 僕のチームのCurtが 「SwiftUI操作マニュアル」のセッションで ナビゲーションスタックと ナビゲーションのスプリットビューについて お話ししています 次に新しい Scene APIについて話しましょう WindowGroupはすでにおなじみでしょう Appのメインインターフェイスを 構築する優れた方法であり 複数のウィンドウを生成して Appのデータに異なる視点を 持たせることができます 今年新たにウィンドウを追加しました 推測できるでしょうか これはウィンドウをAppに搭載します ここではパーティーの総費用を表示する 「Party Budget」ウィンドウを追加しています

    このウィンドウはデフォルトで使用可能で Appのウィンドウメニューで 名前を選択することで表示されます しかしCommand-0の キーボードショートカットを 割り当てればもっと簡単に ウィンドウを開くことができます しっかり予算を管理できるように このウィンドウを表示する ツールバーボタンを追加しておきます EnvironmentのopenWindowを使うことで SwiftUIで管理された新しいウィンドウを コードから開くことができるようになりました デフォルトのサイズ・位置 リサイズ可否などの修飾子を含む 新しいウィンドウのカスタマイズの スイートを今年追加しました パーティーの予算は邪魔にならないように デフォルトでは隅の小さな ウィンドウに表示されます しかしその位置やサイズを調整すると Appを再起動しても SwiftUIは自動的にそれを記憶しています 新しいスタンドアロンのウィンドウシーンは このようなMacの小さな 補助ウィンドウには最適ですが Party Plannerは マルチプラットフォームのAppであり 小さなスクリーンには より適したデザインが必要です そこでiOSではサイズを変更できる シートの中に予算を表示することにしました presentationDetents() 修飾子で設定します 今回は250ポイントと システムで定義された中位の高さの 2種類のサイズから選択する リサイズ可能なシートを設定しました Xcodeのマルチプラットフォームターゲットが SwiftUIベースのAppをサポートしたので 今年はプラットフォーム間での作業が簡単です 1つのターゲットを複数のプラットフォームに デプロイすることができます Xcodeのツールバーのプルダウンメニューから プラットフォームを選択するだけです 詳しくは「Xcodeの最新情報」や 「Xcodeで マルチプラットフォームAppを開発する」 のセッションをご覧ください 最後の新しいシーンタイプでは メニューバーを見てみましょう macOS VenturaではMenuBarExtrasを 完全にSwiftUIで構築できるようになりました! これらはAppの他のシーンタイプと 一緒に定義することができ Appが実行されている間 常にメニューバーに表示されます あるいはApp全体を MenuBarExtraで構築することもできます これは最もシンプルなアイデアを macOS上で実現する とても楽しい方法です 「SwiftUI Appに複数のウィンドウを 追加する」のセッションでは すべての新しいシーンタイプと その機能を利用する方法について より詳しく説明しています 次はウィンドウにコントロールを配置します Frankにバトンタッチです ありがとうNick 今年はインタラクティブなコンテンツを 構築するためのAPIを色々強化しています たくさんあります まずはFormsから macOS Venturaには新しい システム設定Appが搭載されており 先ほどNickが説明した ナビゲーションスプリットビューと スタックを用いて構築された 合理的なナビゲーション構造を持っています また新鮮でモダンなインターフェイス スタイルも採用されています 設定Appのインターフェイスは コントロール部品が多いため このスタイルは 多くのコントロールを含むフォームを 上手に整理し 調和して表示できるように 特別に設計されました そしてParty Planner Appでも この新しいデザインを採用しています 見てみましょう Event Detailsビューではさまざまな種類の コントロールがセクションにまとめられており 設定Appと同じような目的を持っています このためシステム設定と同じ 新しいビジュアルスタイルが最適です

    このデザインを有効にするには macOSの新しいformStyle(.grouped)を使います そしてSwiftUIの宣言型 APIの柔軟性により フォーム内のコンテンツとコントロールは 自動的に新しいスタイルに適応します たとえばセクションは ヘッダーの下にあるコンテンツをグループ化し コントロールは一貫してラベルと値を 先頭と末尾の端に揃えます いくつかのコントロールでは外観も適応します 例えばトグルは 一貫したレイアウトとアライメントにより 末尾にミニスイッチとして表示されます そしてフォーム自体が 多くのパーツを描画するので 他のコントロールはより ライトウェイトな見た目で このコンテキストに適応し ロールオーバーで より目立つ表示となります SwiftUIは他のタイプのコンテンツを この新しいスタイルで簡単にアラインできます 新しいLabeledContent ビューを使用します このビューは新しいコントロールを構築したり ちょっとした読み取り専用の情報を 表示するためも使えます このケースでは イベントの住所を表示しています SwiftUIは自動的にスタイルを調整し テキストを選択可能にしています

    しかしLabeledContentは 住所を全て表示するカスタムビューなど 任意の種類のビューを含めることもできます SwiftUIは他のケースでも より賢くテキストにデフォルトのスタイルを 適用するようになりました タイトルやサブタイトルを形成するために コントロールのラベル内の複数のテキストを 階層的にフォーマットします この新しいフォームのデザインは macOSで素晴らしく見えますが この同じコードの多くを iOS版Appでも共有可能です

    iOSでもいくつかのデザインが 改善されていることに気づかれるでしょう 例えばポップアップメニューのピッカーは macOSのビジュアルスタイルに インスパイアされていますが タッチベースのインターフェイスに 美しくフィットするように インタラクションや外観が最適化されています もちろん同じコードがiPadの 大きなスクリーンでも素晴らしく機能します Macと一緒に SwiftUIの宣言型モデルが 共有インターフェースを構築する際に コードの共有を助け すべてのプラットフォームで役立つことが お分かりいただけると思います もちろんフォームスタイル以外の コントロールも改善しています Party Plannerで使用している その他の新しいコントロールを 見てみましょう まずはiOS版のNew Activityページです テキストフィールドは新しい axisパラメータを使用して 垂直方向に拡大するように設定できます テキストに合わせて高さが拡大され lineLimitが指定された場合は 行の高さの上限が設定されます ineLimit修飾子は最小限のスペースを確保し コンテンツが追加されると拡大し 上限を超えるとスクロールするなど より高度な動作もサポートします テキストフィールドの下には 新しい MultiDatePicker コントロールがあり 連続しない日付の選択をサポートしているので パーティーの活動を分散させることができます

    今回のパーティーのテーマについて 複雑な心境になっているかもしれませんね mixed-stateのコントロールを使用して その感情を表現できるようになりました ここでは1つの集約されたToggleに 折り畳むことができる Toggleのグループを持っています 内側のToggleはそれぞれ 単一のバインディングに対応するのに対し 集約されたToggleはすべてのバインディングの コレクションに対応し それらすべての値が一致しない場合 混合状態を表示します

    Pickersも同じように動作します この装飾テーマピッカーは 現在選択されている装飾を反映してその値を変更します しかし複数の装飾を選択した場合は 装飾のテーマが混在した状態で Mixed-stateインディケーターを表示します ではiOS Appに戻りましょう イベントのハッシュタグを選択するための いくつかのボタン型トグルがあります それぞれのトグルを区別するために 境界線のあるボタンスタイルを追加します このようなボタンスタイルは トグル・メニュー・ピッカーなど ボタンのような外観の コントロールに適用されます 次はSteppers 新たに値のフォーマットを 指定できるようになりました macOSではフォーマットされたステッパーは その値が編集可能なフィールドに表示されます そしてステッパーはwatchOSでも 利用できるようになりました Apple Watchの大好きな新機能の1つです Accessibility Quick Actionsは 手を握る事で起動する新しい代替手段です クイックアクションは他のUIアクションと同様 ボタンを使って定義することができ 同じコードを共有することができます 様々なコントロールの話をしました ですがコントロールだけが インタラクティブな操作ではありません テーブルやリストなど より大きなインタラクティブコンテナの 新機能について見ていきましょう

    iPadOSでテーブルがサポートされたことを お知らせできるのはとてもうれしいことです ご想像のとおりiPadOSのテーブルは 昨年紹介したmacOSと同じTable API なので プラットフォーム間で簡単にコードを 共有することができます Invitationsテーブルには iPadの大きなディスプレイを活かして 各人の名前・都市・招待状のステータスの 3列を表示しています しかしこのテーブルはiPhoneを含む コンパクトサイズクラスでも 適切にレンダリングされ より小さなスクリーンスペースでは 主要なカラムだけを表示します macOSでこのテーブルを見てみましょう 素晴らしい出来栄えですが テーブルの中で一般的なアクションを実行するための コンテクストメニューを追加してみます これを行うのは contentMenu修飾子です forSelectionTypeで指定された タイプと互換性のある テーブルやリストで有効になります メニュービルダーでは現在の選択範囲の コレクションが与えられ 高度なコンテキストメニューを 構築することが可能になります 対象は 選択された1行・複数行 そして テーブルの空の領域をクリックした時のように 選択されていない行に対して操作できます コンテキストメニューは テーブルの中で直接アクションを表示するので スピードと効率に優れています しかしこれらのアクションをもっと 発見しやすくしたいと思います 発見性を向上させる素晴らしい方法は 共通のアクションをツールバーの ボタンとして表示することです iPadOSには新しく洗練された ツールバーデザインがあります iPadのツールバーはユーザーのカスタマイズと 並び替えをサポートできるようになり それぞれのツールバーアイテムに 識別子を提供することによって実装できます macOSと同じAPIです SwiftUI はAppの起動中に これらの識別子を使って カスタムツールバーの設定を自動的に保存し Appの再起時に復元することができます iPadOSではすべてのツールバーアイテムが カスタマイズを許可しているわけではないことに 注意してください カスタマイズ可能なアクションは デフォルトでツールバーの中央に表示される 新しいsecondaryActionツールバーアイテムか またはコンパクトサイズクラスで オーバーフローメニューに含まれる項目です それでは パーティの噂は広まり 参加者が急激に増えているようですね 検索のサポートを追加して テーブルの管理をしやすくしましょう SwiftUIはすでにsearchable修飾子で 基本的な検索をサポートしています そして今年の新機能として検索フィールドは より構造化された検索クエリを構築するのに役立つ トークン化された入力とサジェストをサポートします 結果のフィルタリングを助けるために SwiftUI は現在検索スコープをサポートしており macOS ではツールバーの下のスコープバーで iOS ではナビゲーションバーに セグメント化されて表示されます iPad上のSwiftUIの機能に関して このセッションではほんの一部を紹介しました 詳しくは「iPadのSwiftUI: インターフェイスを構造化する」をご覧ください イベントの詳細管理について 準備ができてきました それではここでニュース配信です 皆さんをワクワクさせましょう 他の人とコンテンツを共有したり App間でデータを共有したりすることは 多くのAppに不可欠な部分です これらの機能を活用することで Appを利用する人々のワークフローに より一層溶け込むことができます 今年はそれをさらに容易にするための エキサイティングな分野がいくつかあります まずは写真やビデオを選択するための 新しいPhotosPickerから始めましょう マルチプラットフォーム対応で プライバシーも守れます 写真はパーティーに欠かせないものなので 撮影した写真に 楽しい誕生日エフェクトを加える機能を Party Planner Appに追加しました 新しい PhotosPicker ビューは Appの任意の場所に配置でき 起動時に標準的な写真選択の UI を表示して ユーザーのライブラリから 写真やビデオを選択します PhotosPickerは選択されたアイテムへの バインディングを受け取り 実際の写真やビデオデータへの アクセスが提供されます

    またコンテンツの種類のフィルタリングや 好みの写真エンコーディングなど 豊富な設定オプションが追加されています

    今まで見た中で一番映えるカップケーキだ でも1個じゃ足りない スペシャルエフェクトをかけながら 次に進みましょう カスタマイズした写真ができたので 新しいShareLink APIを使って 写真を共有する準備ができました 各プラットフォームにはあなたのAppから コンテンツを共有できるようにするための 標準的なインターフェイスがあります watchOS 9では Watch Appの中から 共有シートを表示できます 新しいShareLinkビューは そのシステムの共有シートを App内から提示することを可能にします 共有したいコンテンツと 共有シートで使用する プレビューを提供するだけで 標準的な共有アイコンボタンが 自動的に作成されます

    タップすると標準の共有シートが表示され コンテンツを送信することができます 共有リンクはコンテキストメニューや プラットフォーム間など 適用されたコンテキストに適応します PhotosPickerやShareLinkなどはすべて 新しいTransferableプロトコルを 利用しています これはApp間で型を転送する方法を 記述するための Swift初の宣言的な方法です Transferable プロトコルは ドラッグアンドドロップのような SwiftUIの機能を強化するために使用され 他のAppからParty Plannerのギャラリーに 画像を簡単にドロップすることができます これは新しい dropDestination API を利用し ペイロード型を受けとります この場合は単なる画像です 完了ブロックでは受け取った画像と ドロップ先のコレクションを提供します

    文字列や画像など多くの標準的な型は すでにTransferableに準拠しています ですのでこのAppに実装するのは それほど大変な作業ではありませんでしたが 独自のカスタムタイプをTransferableに 準拠させることも簡単にできます その際にはCodableサポートや カスタムコンテンツタイプを使うなど 自分のタイプに適した表現方法をで宣言します Transferable やその他の表現 高度なヒントについてもっと知りたい方は 「Transferableの紹介」をご覧ください カップケーキの準備の間に Nickが色々準備しています そっちはどう? もうすぐだ このパーティホーンを 完全にカスタマイズします でももう少し時間が必要です まずはグラフィックの話をしましょう リッチなグラフィック効果を実現するために 今年ShapeStyleは新しいAPIを備えました このAPIでゲストカードをポップに仕上げます Colorには新しいグラデーションプロパティがあり 色から派生した微妙な グラデーションを追加します システムカラーとの相性も抜群です

    ShapeStyleには 新しいshadow修飾子が追加されました これを白の前景スタイルに追加すると テキストとシンボルに影が追加されます このシャドウのディテールは注目に値します ドロップシャドウはカレンダーシンボルの すべての要素に適用されました

    SF Symbolsの全てと 新しいSwiftUI ShapeStyle エクステンションを使えば ゴージャスなアイコンを作ることができます

    それではSF Symbolsを パーティへ持ち込みましょう 今年いくつかの素晴らしい改良がなされた SwiftUIプレビューを使用して見てみます プレビューは今までも同時に複数の構成を 同時にビューを見るための便利な方法でした Xcode 14ではプレビューのバリアントによって これまで以上に簡単にできるようになりました これにより設定コードを記述することなく 複数の外観・文字サイズ・方向で 同時にビューを開発することができます 同じグラデーションをもう一度使うことも 楕円形のグラデーションにして 画像に柔らかな輝きを持たせることも 暗い色と明るい色のアピアランスで プレビューすることもできます

    プレビューはデフォルトで ライブモードで実行されるようになりました ちょっとしたダンスがなければ 素晴らしい誕生日パーティーとは言えないので このSFシンボルを踊らせましょう ♪ Electronic dance music ♪ ♪

    このアイコンが大変身します SwiftUIはテキストと画像のアニメーションを 次のレベルまで引き上げました そのテキストのアニメーションを もう一度スローモーションで見てみましょう テキストはウェイト・スタイル・レイアウトで 美しくアニメーションさせることが できるようになりました そして最も良いところは SwiftUIの他の部分のアニメーションと 同じAPIを利用しているところです UIプログラミングで私が一番好きな部分 レイアウトについて話して 締めくくりましょう SwiftUIはビューをレイアウトするための 新しい方法を追加しました Gridは新しいコンテナビューで 2次元のグリッドにビューを配置します グリッドは複数の列にまたがるセルを有効にし 行と列をまたぐ自動的な整列を可能にするために そのサブビューを前もって測定します 実はグリッドについては以前にすでに見ています

    Grid・GridRow・gridCellColumns修飾子で グリッドを少しずつ構築できます もちろんSwiftUIのすべてのレイアウトと同じように これらは組み合わせるために構築されています 最初にSwiftUIのレイアウトモデルを導入し 一般的なレイアウトを実現するために 原始的なレイアウトタイプの ツールボックスを提供しました ほとんどの場合これらのレイアウトタイプで 仕事を終わらせることができますが 時には サイズ minX frame.origin.x - frame.midX / 2 + 3 といったレイアウトコードが必要です そんな時に 新しいLayoutプロトコルを使います これを使えばあなた自身のファーストクラスの レイアウトを構築するために SwiftUIのスタックとグリッドを 実装するために使用した 完全なパワーと柔軟性を 手に入れることができます Layoutを使用してパーティーのゲストのために この特注の座席表のレイアウトを構築しました 配置はどうする? これがあれば迷いません Layoutプロトコルを使用すると ビュー階層の特定のニーズに合わせて あらゆる種類の効率的なレイアウトを 構築できます Layoutを採用する方法と 他の新しい素晴らしいテクニックについては 「SwiftUIによるカスタムレイアウトの作成」 のセッションをご覧ください。 このLayoutをあなたの為に作りました 新しい AnyLayout タイプを使用して Grid レイアウトと 私が書いたカスタム散布レイアウトを 切り替えることができます 終わりに サプライズが残っています あなたを招待します ♪ SwiftUIの誕生日パーティへようこそ 新しいAPIを私たちと共に 私たちがカバーしたAPIには 話しきれなかった詳細が残されており 今日カバーできなかった APIもたくさんあります パーティを そしてWWDC 2022を楽しんで 僕たちはケーキを楽しみます メロウなヒップホップ ♪

    • 2:51 - Swift Charts: Required models and extensions

      import Foundation
      import SwiftUI
      
      // MARK: - Party Planner Models
      enum PartyTask: String, Identifiable, CaseIterable, Hashable {
          case food = "Food"
          case music = "Music"
          case supplies = "Supplies"
          case invitations = "Invitations"
          case eventDetails = "Event Details"
          case activities = "Activities"
          case funProjection = "Fun Projection"
          case vips = "VIPs"
          case photosFilter = "Photos Filter"
      
          var name: String { rawValue }
        
         var color: Color {
             switch self {
             case .food:
                  return palette[0]
             case .supplies:
                  return palette[1]
             case .invitations:
                  return palette[2]
             case .eventDetails:
                  return palette[3]
             case .funProjection:
                  return palette[4]
             case .activities:
                  return palette[5]
             case .vips:
                  return palette[6]
             case .music:
                  return palette[7]
             case .photosFilter:
                  return palette[8]
             }
          }
      
          var imageName: String {
              switch self {
              case .food:
                  return "birthday.cake"
              case .supplies:
                  return  "party.popper"
              case .invitations:
                  return "envelope.open"
              case .eventDetails:
                  return "calendar.badge.clock"
              case .funProjection:
                  return "gauge.medium"
              case .activities:
                  return "bubbles.and.sparkles"
              case .vips:
                  return "person.2"
              case .music:
                  return  "music.mic"
              case .photosFilter:
                  return "camera.filters"
              }
          }
      
          var id: String { rawValue }
      
          var subtitle: String {
              switch self {
              case .food:
                  return "Apps, 'Zerts and Cakes"
              case .supplies:
                  return "Streamers, Plates, Cups"
              case .invitations:
                  return "Sendable, Non-Transferable"
              case .eventDetails:
                  return "Date, Duration, And Placement"
              case .funProjection:
                  return "Beta — How Fun Will Your Party Be?"
              case .activities:
                  return "Dancing, Paired Programing"
              case .vips:
                  return "User Interactive Guests"
              case .music:
                  return "Song Requests & Karaoke"
              case .photosFilter:
                  return "Filtering and Mapping"
              }
          }
      
          var emoji: String {
              switch self {
              case .food:
                  return "🎂"
              case .music:
                  return "🎤"
              case .supplies:
                  return "🎉"
              case .invitations:
                  return "📨"
              case .eventDetails:
                  return "🗓"
              case .funProjection:
                  return "🧭"
              case .activities:
                  return "💃"
              case .vips:
                  return "⭐️"
              case .photosFilter:
                  return "📸"
              }
          }
      }
      
      private let palette: [Color] = [
          Color(red: 0.73, green: 0.20, blue: 0.20),
          Color(red: 0.95, green: 0.66, blue: 0.24),
          Color(red: 0.14, green: 0.29, blue: 0.49),
          Color(red: 0.46, green: 0.76, blue: 0.67),
          Color(red: 0.30, green: 0.33, blue: 0.22),
          Color(red: 0.49, green: 0.55, blue: 0.64),
          Color(red: 0.92, green: 0.53, blue: 0.30),
          Color(red: 0.20, green: 0.45, blue: 0.55),
          Color(red: 0.41, green: 0.45, blue: 0.45),
          Color(red: 0.87, green: 0.67, blue: 0.61)
      ]
      
      // MARK: - Swift Charts Models
      
      struct RemainingPartyTask: Identifiable {
          let category: PartyTask
          let date: Date
          let remainingCount: Int
      
          let id = UUID()
      }
      
      let remainingSupplies: [RemainingPartyTask] = [
          RemainingPartyTask(category: .supplies, date: .daysAgo(4), remainingCount: 10),
          RemainingPartyTask(category: .supplies, date: .daysAgo(3), remainingCount: 11),
          RemainingPartyTask(category: .supplies, date: .daysAgo(2), remainingCount: 9),
          RemainingPartyTask(category: .supplies, date: .daysAgo(1), remainingCount: 4),
          RemainingPartyTask(category: .supplies, date: .daysAgo(0), remainingCount: 1),
      ]
      
      let remainingInvitations: [RemainingPartyTask] = [
          RemainingPartyTask(category: .invitations, date: .daysAgo(4), remainingCount: 14),
          RemainingPartyTask(category: .invitations, date: .daysAgo(3), remainingCount: 13),
          RemainingPartyTask(category: .invitations, date: .daysAgo(2), remainingCount: 11),
          RemainingPartyTask(category: .invitations, date: .daysAgo(1), remainingCount: 6),
          RemainingPartyTask(category: .invitations, date: .daysAgo(0), remainingCount: 4),
      ]
      
      let remainingActivities: [RemainingPartyTask] = [
          RemainingPartyTask(category: .activities, date: .daysAgo(4), remainingCount: 6),
          RemainingPartyTask(category: .activities, date: .daysAgo(3), remainingCount: 7),
          RemainingPartyTask(category: .activities, date: .daysAgo(2), remainingCount: 4),
          RemainingPartyTask(category: .activities, date: .daysAgo(1), remainingCount: 2),
          RemainingPartyTask(category: .activities, date: .daysAgo(0), remainingCount: 1),
      ]
      
      let remainingVenue: [RemainingPartyTask] = [
          RemainingPartyTask(category: .eventDetails, date: .daysAgo(4), remainingCount: 4),
          RemainingPartyTask(category: .eventDetails, date: .daysAgo(3), remainingCount: 5),
          RemainingPartyTask(category: .eventDetails, date: .daysAgo(2), remainingCount: 7),
          RemainingPartyTask(category: .eventDetails, date: .daysAgo(1), remainingCount: 4),
          RemainingPartyTask(category: .eventDetails, date: .daysAgo(0), remainingCount: 2)
      ]
      
      let partyTasksRemaining: [RemainingPartyTask] = [remainingVenue,
                                                      remainingActivities,
                                                      remainingInvitations,
                                                      remainingSupplies
      ].flatMap { $0 }
      
      // MARK: Date Utilities
      
      extension Date {
          static func daysAgo(_ daysAgo: Int) -> Date {
              Calendar.current.date(byAdding: .day, value: -daysAgo, to: Date())!
          }
      
          func daysEqual(_ other: Date) -> Bool {
              Calendar.current.dateComponents([.day], from: self, to: other).day == 0
          }
      }
      
      extension Date {
          static let wwdc22: Date = DateComponents(
              calendar: .autoupdatingCurrent,
              timeZone: TimeZone(identifier: "PST"),
              year: 2022,
              month: 6,
              day: 6,
              hour: 9,
              minute: 41,
              second: 00).date!
      }
    • 2:56 - Swift Charts: Bar Chart 1

      Chart(partyTasksRemaining) {
          BarMark(
              x: .value("Date", $0.date, unit: .day),
              y: .value("Tasks Remaining", $0.remainingCount)
          )
      }
      .padding()
    • 3:33 - Swift Charts: Bar chart 2

      var body: some View {
          Chart(partyTasksRemaining) {
              BarMark(
                  x: .value("Date", $0.date, unit: .day),
                  y: .value("Tasks Remaining", $0.remainingCount)
              )
          }
          .padding()
      }
    • 3:53 - Swift Charts: LineMark

      var body: some View {
          Chart(partyTasksRemaining) {
              LineMark(
                  x: .value("Date", $0.date, unit: .day),
                  y: .value("Tasks Remaining", $0.remainingCount)
              )
              .foregroundStyle(by: .value("Category", $0.category))
          }
          .padding()
      }
    • 4:08 - Swift Charts: Line Chart with Symbols

      var body: some View {
          Chart(partyTasksRemaining) {
              LineMark(
                  x: .value("Date", $0.date, unit: .day),
                  y: .value("Tasks Remaining", $0.remainingCount)
              )
              .foregroundStyle(by: .value("Category", $0.category))
              .symbol(by: .value("Category", $0.category))
          }
          .padding()
      }
    • 4:39 - Swift Charts: Annotations

      var body: some View {
          Chart {
              ForEach(partyTasksRemaining) { task in
                  LineMark(
                      x: .value("Date", task.date, unit: .day),
                      y: .value("Tasks Remaining", task.remainingCount)
                  )
                  .foregroundStyle(by: .value("Category", task.category))
                  .symbol(by: .value("Category", task.category))
                  .annotation(position: .leading) {
                      Text("\(task.category.emoji)")
                  }
              }
      
              RuleMark(y: .value("Value", 5))
                  .foregroundStyle(.red)
                  .lineStyle(StrokeStyle(lineWidth: 2.0, dash: [4, 5]))
                  .annotation(position: .top, alignment: .trailing) {
                      VStack(alignment: .trailing) {
                          Text("Today's Goal")
                          Text("Status: ✔︎")
                      }
                      .font(.caption)
                      .foregroundColor(.gray)
                      .padding(.trailing, 2)
                  }
          }
      }
    • 6:15 - Food Models

      import Foundation
      
      // MARK: Food Models
      
      /// A model representing a food with a price and quantity.
      struct FoodItem: Hashable, Identifiable, Codable, Equatable {
          let emoji: String
          let name: String
          var description: String = ""
          let price: Decimal
          var quantity: Int = 0
          var id: String { name }
      }
      
      let donut = FoodItem(emoji: "🍩", name: "Doughnut", description: "Yeast, Old-fashioned, Cake, and the dubious Apple Fritter", price: 2.35, quantity: 6)
      let moonCake = FoodItem(emoji: "🥮", name: "Moon Cake", description: "Lotus seed paste — plenty of crust", price: 2.20, quantity: 4)
      let shavedIce = FoodItem(emoji: "🍧", name: "Shaved Ice", description: "Shave your own ice!", price: 3.25, quantity: 1)
      let cupcake = FoodItem(emoji: "🧁", name: "Cupcake", description: "Also goes by the name Cake Nano", price: 4.00, quantity: 5)
      let flan = FoodItem(emoji: "🍮", name: "Flan", description: "What's in a flan? That which we call milk, eggs, and sugar by any other name would taste just as sweet.", price: 6.50, quantity: 2)
      let taffy = FoodItem(emoji: "🍬", name: "Taffy", description: "Freshwater, actually.", price: 1.00, quantity: 11)
      let cake = FoodItem(emoji: "🎂", name: "Cake Cake", description: "The real deal", price: 15.00, quantity: 1)
      let cookie = FoodItem(emoji: "🍪", name: "Cookie Cake", description: "The ultimate dessert", price: 4.30, quantity: 1)
      
      let relatedFoods = [donut, moonCake, shavedIce, cupcake, flan, taffy, cake, cookie]
      
      extension Array where Element: Equatable {
      
          /// A quick-and-dirty way of getting a random few elements from an Array that don't include a single,
          /// particular element.
          /// - Parameters:
          ///   - count: The number of desired random elements, must be less than `Array.count`
          ///   - except: Filter out this particular element    
          func random(_ count: Int, except: Element) -> [Element] {
              assert(count >= count)
              var copy = self
              copy.shuffle()
              copy.removeAll(where: { $0 == except })
              return Array(copy[0..<count])
          }
      }
      
      let partyFoods = [
          FoodItem(emoji: "🍨", name: "Ice Cream",
                   price: 3.50, quantity: 4),
          flan,
          taffy,
          donut,
          FoodItem(emoji: "🍉", name: "Watermelon",
                   price: 3.65, quantity: 1),
          FoodItem(emoji: "🍒", name: "Cherries",
                   price: 8.00, quantity: 1),
          cupcake,
          cookie,
          FoodItem(emoji: "🍥", name: "Fish Cake",
                   price: 5.00, quantity: 2),
          moonCake,
          cake,
          FoodItem(emoji: "🍘", name: "Rice Cracker",
                   price: 0.25, quantity: 16),
          FoodItem(emoji: "🥨", name: "Pretzels",
                   price: 3.00, quantity: 3),
          shavedIce,
          FoodItem(emoji: "🥧", name: "Apple Pie",
                   price: 4.10, quantity: 1)
      ]
    • 6:21 - NavigationStack with view-based NavigationLinks

      // MARK: NavigationStack with View-based NavigationLinks
      
      struct FoodsListView: View {
          fileprivate var foodItems = partyFoods
          @State private var selectedFoodItems: [FoodItem] = []
      
          var body: some View {
              NavigationStack {
                  List(foodItems) { item in
                      NavigationLink {
                          FoodDetailView(item: item)
                      } label: {
                          FoodRow(food: item)
                      }
                  }
                  .navigationTitle("Party Food")
      
              }
          }
      }
      
      struct FoodRow: View {
          let food: FoodItem
      
          var body: some View {
              HStack {
                  Text(food.emoji)
                      .font(.system(size: 15))
                      .foregroundStyle(.secondary)
                  Text(food.name)
                      .font(.caption)
                      .bold()
                  Spacer()
                  Text("\(food.quantity)")
              }
          }
      }
      
      struct FoodDetailView: View {
          let item: FoodItem
      
          var body: some View {
              ScrollView {
                  VStack {
                      HStack {
                          Text(item.emoji)
                              .font(.system(size: 30))
                          Text(item.name)
                              .font(.title3)
                      }
                      .padding(.bottom, 4)
                      Text(item.description)
                          .font(.caption)
                      Divider()
                      RelatedFoodsView(relatedFoods: relatedFoods.random(3, except: item))
                  }
              }
          }
      }
      
      struct RelatedFoodsView: View {
          @State var relatedFoods: [FoodItem]
      
          var body: some View {
              VStack {
                  Text("Related Foods")
                      .background(.background, in: RoundedRectangle(cornerRadius: 2))
                  HStack {
                      ForEach(relatedFoods) { food in                    
                          NavigationLink {
                              FoodDetailView(item: food)
                          } label: { Text(food.emoji) }
                      }
                  }
              }
          }
      }
    • 6:51 - NavigationStack with value-based NavigationLinks

      // MARK: NavigationStack with Value-based Navigation Links
      
      struct FoodsListView: View {
          fileprivate var foodItems = partyFoods
          @State private var selectedFoodItems: [FoodItem] = []
      
          var body: some View {
              NavigationStack(path: $selectedFoodItems) {
                  List(foodItems) { item in
                      NavigationLink(value: item) {
                          FoodRow(food: item)
                      }
                  }
                  .navigationTitle("Party Food")
                  .navigationDestination(for: FoodItem.self) { item in
                      FoodDetailView(item: item, path: $selectedFoodItems)
                  }
              }
          }
      }
      
      struct FoodDetailView: View {
          let item: FoodItem
          @Binding var path: [FoodItem]
      
          var body: some View {
              ScrollView {
                  VStack {
                      HStack {
                          Text(item.emoji)
                              .font(.system(size: 30))
                          Text(item.name)
                              .font(.title3)
                      }
                      .padding(.bottom, 4)
                      Text(item.description)
                          .font(.caption)
                      Divider()
                      RelatedFoodsView(relatedFoods: relatedFoods.random(3, except: item))
                      if path.count > 1 {
                          Button("Back to First Item") { path.removeSubrange(1...) }
                      }
                  }
              }
          }
      }
      
      struct RelatedFoodsView: View {
          @State var relatedFoods: [FoodItem]
      
          var body: some View {
              VStack {
                  Text("Related Foods")
                      .background(.background, in: RoundedRectangle(cornerRadius: 2))
                  HStack {
                      ForEach(relatedFoods) { food in
                          NavigationLink(value: food) {
                              Text(food.emoji)
                          }
                      }
                  }
              }
          }
      }
    • 8:16 - NavigationSplitView

      // MARK: NavigationSplitView Demo
      
      struct PartyPlannerHome: View {
          @State private var selectedTask: PartyTask?
      
          var body: some View {
              NavigationSplitView {
                  List(PartyTask.allCases, selection: $selectedTask) { task in
                      NavigationLink(value: task) {
                          TaskLabel(task: task)
                      }
      						    .listItemTint(task.color)
                  }
              } detail: {
                  selectedTask.flatMap { $0.color } ?? .white
              }
          }
      }
      
      struct TaskLabel: View {
          let task: PartyTask
      
          var body: some View {
              Label {
                  VStack(alignment: .leading) {
                      Text(task.name)
                      Text(task.subtitle)
                          .font(.footnote)
                          .foregroundStyle(.secondary)
                  }
              } icon: {
                  Image(systemName: task.imageName)
                      .symbolVariant(.circle.fill)
              }
          }
      }
    • 9:13 - Navigation split and stack composition

      struct PartyPlannerHome: View {
          @State private var selectedTask: PartyTask?
      
          var body: some View {
              NavigationSplitView {
                  List(PartyTask.allCases, selection: $selectedTask) { task in
                      NavigationLink(value: task) {
                          TaskLabel(task: task)
                      }
                      .listItemTint(task.color)
                  }
              } detail: {
                  if case .food = selectedTask {
                      FoodsListView()
                  } else {
                      selectedTask.flatMap { $0.color } ?? .white
                  }
              }
          }
      }
    • 10:10 - Window

      @main
      struct PartyPlanner: App {
          var body: some Scene {
              WindowGroup("Party Planner") {
                  PartyPlannerHome()
              }
      
              Window("Party Budget", id: "budget") {
                  Text("Budget View")
              }
              .keyboardShortcut("0")
          }
      }
    • 10:42 - Open window

      struct DetailView: View {
          @Environment(\.openWindow) var openWindow
      
          var body: some View {
              Text("Detail View")
                  .toolbar {
                      Button {
                          openWindow(id: "budget")
                      } label: {
                          Image(systemName: "dollarsign")
                      }
                  }
          }
      }
    • 11:00 - Window customizations

      @main
      struct PartyPlanner: App {
          var body: some Scene {
              WindowGroup("Party Planner") {
                  PartyPlannerHome()
              }
      
              Window("Party Budget", id: "budget") {
                  Text("Budget View")
              }
              .keyboardShortcut("0")
              .defaultPosition(.topLeading)
              .defaultSize(width: 220, height: 250)
          }
      }
    • 11:47 - Resizable sheets

      struct PartyPlannerHome: View {
          @State private var selectedTask: PartyTask?
          @State private var presented: Bool = false
      
          var body: some View {
              NavigationSplitView {
                  List(PartyTask.allCases, selection: $selectedTask) { task in
                      NavigationLink(value: task) {
                          TaskLabel(task: task)
                      }
                      .listItemTint(task.color)
                  }
              } detail: {
                  if case .food = selectedTask {
                      FoodsListView()
                  } else {
                      selectedTask.flatMap { $0.color } ?? .white
                  }
              }
              .sheet(isPresented: $presented) {
                  Text("Budget View")
                      .presentationDetents([.height(250), .medium])
                      .presentationDragIndicator(.visible)
              }
          }
      }
    • 12:51 - Menu bar extras

      @main
      struct PartyPlanner: App {
          var body: some Scene {
              Window("Party Budget", id: "budget") {
                  Text("Budget View")
              }
      
              MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
                  BulletinBoard()
              }
              .menuBarExtraStyle(.window)
          }
      }
      
      
      
      private let allPosts: [String] = [
          "Did you know: On your third birthday, you are celebrating your 4.0 release.",
      ]
      
      struct BulletinBoard: View {
      
          @State var currentPostIndex: Int = 0
      
          var currentPost: String {
              allPosts[currentPostIndex]
          }
      
          var body: some View {
              VStack(spacing: 16) {
      
                  VStack(spacing: 12) {
                      HStack(alignment: .firstTextBaseline) {
                          Text("“")
                              .font(.custom("Helvetica", size: 50).bold())
                              .baselineOffset(-23)
                              .foregroundStyle(.tertiary)
      
                          Text("Party Bulletin Board")
                              .font(.headline.weight(.semibold))
                              .foregroundStyle(.secondary)
      
                          Spacer()
      
                          Text("June 6, 2022")
                              .font(.headline.weight(.regular))
                              .foregroundStyle(.secondary)
                      }
                      .frame(height: 20)
      
      
                      Text(currentPost)
                          .font(.system(size: 18))
                          .multilineTextAlignment(.center)
                  }
                  .padding(.bottom, 4)
      
                  Divider()
      
                  HStack {
                      Button {
      
                      } label: {
                          Label("Calendar", systemImage: "calendar")
                      }
                      Button {
                          currentPostIndex = (currentPostIndex + 1) % allPosts.count
                      } label: {
                          Text("Previous")
                              .frame(maxWidth: .infinity)
                      }
      
                      ShareLink(items: [currentPost])
                  }
                  .labelStyle(.iconOnly)
                  .controlSize(.large)
              }
              .padding(16)
          }
      }
    • 12:58 - Menu bar extra app

      @main
      struct MessageBoard: App {
          var body: some Scene {
              MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
                  BulletinBoard()
              }
              .menuBarExtraStyle(.window)
          }
      }
    • 14:25 - Grouped forms

      struct ContentView: View {
          enum Theme: String, CaseIterable, Identifiable {
              var id: String { self.rawValue }
              case blue, gold, black, white
      
              var swatch: some View {
                  Circle()
                      .fill(color)
                      .overlay {
                          Circle().stroke(.tertiary)
                      }
                      .frame(width: 15, height: 15)
              }
      
              var color: Color {
                  switch self {
                  case .blue: return .blue
                  case .gold: return .yellow
                  case .black: return .black
                  case .white: return .white
                  }
              }
          }
      
          enum ColorScheme: String {
              case light, dark
          }
      
          enum Decoration: String, CaseIterable {
              case balloon, confetti, inflatables, noisemakers, all, none
          }
      
          private let address = "One Apple Park Way"
      
          @State private var date: Date = DateComponents(
              calendar: .current, timeZone: .current, year: 2022, month: 6, day: 6
          ).date!
          @State private var eventDescription: String =
              "Come and join us celebrate SwiftUI's birthday party!\n🎉🎂"
      
          @State private var scheme: ColorScheme = .light
          @State private var accent: Theme = .blue
          @State private var extraGuests = false
          @State private var spacesCount: Float = 2
      
          @State private var includeBalloons = false
          @State private var includeConfetti = false
          @State private var includeInflatables = false
          @State private var includeBlowers = false
      
          @State private var selectedDecorations: [Decoration] = []
          @State private var decorationThemes: [Decoration: Theme] = [
              .balloon : .blue,
              .confetti: .gold,
              .inflatables: .black,
              .noisemakers: .white,
              .none: .black
          ]
      
          private var themes: [Binding<Theme>] {
              if selectedDecorations.count == 0 {
                  return [Binding($decorationThemes[.none])!]
              }
              return selectedDecorations.compactMap {
                  Binding($decorationThemes[$0])
              }
          }
      
          var body: some View {
              Form {
                  Section {
                      LabeledContent("Location", value: address)
                      DatePicker("Date", selection: $date)
                      TextField("Description", text: $eventDescription, axis: .vertical)
                          .lineLimit(3, reservesSpace: true)
                  }
      
                  Section("Vibe") {
                      Picker("Accent color", selection: $accent) {
                          ForEach(Theme.allCases) { theme in
                              Text(theme.rawValue.capitalized).tag(theme)
                          }
                      }
                      Picker("Color scheme", selection: $scheme) {
                          Text("Light").tag(ColorScheme.light)
                          Text("Dark").tag(ColorScheme.dark)
                      }
                      #if os(macOS)
                      .pickerStyle(.inline)
                      #endif
                      Toggle(isOn: $extraGuests) {
                          Text("Allow extra guests")
                          Text("The more the merrier!")
                      }
                      if extraGuests {
                          Stepper("Guests limit", value: $spacesCount, format: .number)
                      }
                  }
      
                  Section("Decorations") {
                      Section {
                          List(selection: $selectedDecorations) {
                              DisclosureGroup {
                                  HStack {
                                      Toggle("Balloons 🎈", isOn: $includeBalloons)
                                      Spacer()
                                      decorationThemes[.balloon].map { $0.swatch }
                                  }
                                  .tag(Decoration.balloon)
      
                                  HStack {
                                      Toggle("Confetti 🎊", isOn: $includeConfetti)
                                      Spacer()
                                      decorationThemes[.confetti].map { $0.swatch }
                                  }
                                  .tag(Decoration.confetti)
      
                                  HStack {
                                      Toggle("Inflatables 🪅", isOn: $includeInflatables)
                                      Spacer()
                                      decorationThemes[.inflatables].map { $0.swatch }
                                  }
                                  .tag(Decoration.inflatables)
      
                                  HStack {
                                      Toggle("Party Horns 🥳", isOn: $includeBlowers)
                                      Spacer()
                                      decorationThemes[.noisemakers].map { $0.swatch }
                                  }
                                  .tag(Decoration.noisemakers)
                              } label: {
                                  Toggle("All Decorations", isOn: [
                                      $includeBalloons, $includeConfetti,
                                      $includeInflatables, $includeBlowers
                                  ])
                                  .tag(Decoration.all)
                              }
                              #if os(macOS)
                              .toggleStyle(.checkbox)
                              #endif
                          }
      
                          Picker("Decoration theme", selection: themes) {
                              Text("Blue").tag(Theme.blue)
                              Text("Black").tag(Theme.black)
                              Text("Gold").tag(Theme.gold)
                              Text("White").tag(Theme.white)
                          }
                          #if os(macOS)
                          .pickerStyle(.radioGroup)
                          #endif
                      }
                  }
      
              }
              .formStyle(.grouped)
          }
      }
    • 15:45 - Grouped forms with LabeledContent wrapping a view.

      struct ContentView: View {
          enum Theme: String, CaseIterable, Identifiable {
              var id: String { self.rawValue }
              case blue, gold, black, white
      
              var swatch: some View {
                  Circle()
                      .fill(color)
                      .overlay {
                          Circle().stroke(.tertiary)
                      }
                      .frame(width: 15, height: 15)
              }
      
              var color: Color {
                  switch self {
                  case .blue: return .blue
                  case .gold: return .yellow
                  case .black: return .black
                  case .white: return .white
                  }
              }
          }
      
          enum ColorScheme: String {
              case light, dark
          }
      
          enum Decoration: String, CaseIterable {
              case balloon, confetti, inflatables, noisemakers, all, none
          }
      
          private let location = Location(
              firstLine: "One Apple Park Way", secondLine: "Cupertino, CA 95014")
      
          @State private var date: Date = DateComponents(
              calendar: .current, timeZone: .current, year: 2022, month: 6, day: 6
          ).date!
          @State private var eventDescription: String =
              "Come and join us celebrate SwiftUI's birthday party!\n🎉🎂"
      
          @State private var scheme: ColorScheme = .light
          @State private var accent: Theme = .blue
          @State private var extraGuests = false
          @State private var spacesCount: Float = 2
      
          @State private var includeBalloons = false
          @State private var includeConfetti = false
          @State private var includeInflatables = false
          @State private var includeBlowers = false
      
          @State private var selectedDecorations: [Decoration] = []
          @State private var decorationThemes: [Decoration: Theme] = [
              .balloon : .blue,
              .confetti: .gold,
              .inflatables: .black,
              .noisemakers: .white,
              .none: .black
          ]
      
          private var themes: [Binding<Theme>] {
              if selectedDecorations.count == 0 {
                  return [Binding($decorationThemes[.none])!]
              }
              return selectedDecorations.compactMap {
                  Binding($decorationThemes[$0])
              }
          }
      
          var body: some View {
              Form {
                  Section {
                      LabeledContent("Location") {
                          AddressView(location)
                      }
                      DatePicker("Date", selection: $date)
                      TextField("Description", text: $eventDescription, axis: .vertical)
                          .lineLimit(3, reservesSpace: true)
                  }
      
                  Section("Vibe") {
                      Picker("Accent color", selection: $accent) {
                          ForEach(Theme.allCases) { accent in
                              Text(accent.rawValue.capitalized).tag(accent)
                          }
                      }
                      Picker("Color scheme", selection: $scheme) {
                          Text("Light").tag(ColorScheme.light)
                          Text("Dark").tag(ColorScheme.dark)
                      }
                      #if os(macOS)
                      .pickerStyle(.inline)
                      #endif
                      Toggle(isOn: $extraGuests) {
                          Text("Allow extra guests")
                          Text("The more the merrier!")
                      }
                      if extraGuests {
                          Stepper("Guests limit", value: $spacesCount, format: .number)
                      }
                  }
      
                  Section("Decorations") {
                      Section {
                          List(selection: $selectedDecorations) {
                              DisclosureGroup {
                                  HStack {
                                      Toggle("Balloons 🎈", isOn: $includeBalloons)
                                      Spacer()
                                      decorationThemes[.balloon].map { $0.swatch }
                                  }
                                  .tag(Decoration.balloon)
      
                                  HStack {
                                      Toggle("Confetti 🎊", isOn: $includeConfetti)
                                      Spacer()
                                      decorationThemes[.confetti].map { $0.swatch }
                                  }
                                  .tag(Decoration.confetti)
      
                                  HStack {
                                      Toggle("Inflatables 🪅", isOn: $includeInflatables)
                                      Spacer()
                                      decorationThemes[.inflatables].map { $0.swatch }
                                  }
                                  .tag(Decoration.inflatables)
      
                                  HStack {
                                      Toggle("Party Horns 🥳", isOn: $includeBlowers)
                                      Spacer()
                                      decorationThemes[.noisemakers].map { $0.swatch }
                                  }
                                  .tag(Decoration.noisemakers)
                              } label: {
                                  Toggle("All Decorations", isOn: [
                                      $includeBalloons, $includeConfetti,
                                      $includeInflatables, $includeBlowers
                                  ])
                                  .tag(Decoration.all)
                              }
                              #if os(macOS)
                              .toggleStyle(.checkbox)
                              #endif
                          }
      
                          Picker("Decoration theme", selection: themes) {
                              Text("Blue").tag(Theme.blue)
                              Text("Black").tag(Theme.black)
                              Text("Gold").tag(Theme.gold)
                              Text("White").tag(Theme.white)
                          }
                          #if os(macOS)
                          .pickerStyle(.radioGroup)
                          #endif
                      }
                  }
      
              }
              .formStyle(.grouped)
          }
      }
      
      
      struct AddressView: View {
          private let location: Location
      
          init(_ location: Location) {
              self.location = location
          }
      
          var body: some View {
              VStack {
                  Text(location.firstLine)
                  Text(location.secondLine)
              }
          }
      }
      
      struct Location {
          let firstLine: String
          let secondLine: String
      }
    • 17:06 - Multiline text fields

      struct ContentView: View {
          @State private var activityDates: Set<DateComponents> = [
              DateComponents(calendar: .current, year: 2022, month: 6, day: 6),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 9),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 10)
          ]
          @State private var title: String = .init()
          @State private var description: String = """
                      Join us, and let's force unwrap SwiftUl's
                      birthday presents. Note that although
                      this activity is optional, we may have
                      guards at the entry.
                      """
      
          var body: some View {
              NavigationStack {
                  Form {
                      Section {
                          TextField("Title", text: $title)
                          TextField("Description", text: $description, axis: .vertical)
                      }
                      Section("Dates") {
                          MultiDatePicker("Activities Dates", selection: $activityDates)
                      }
                  }
                  .navigationTitle("New Activity")
                  .toolbar {
                      Button("Save") {}
                  }
              }
          }
      }
    • 17:20 - Multiline text fields with line limit

      struct ContentView: View {
          @State private var activityDates: Set<DateComponents> = [
              DateComponents(calendar: .current, year: 2022, month: 6, day: 6),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 9),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 10)
          ]
          @State private var title: String = .init()
          @State private var description: String = """
                      Join us, and let's force unwrap SwiftUl's
                      birthday presents. Note that although
                      this activity is optional, we may have
                      guards at the entry.
                      """
      
          var body: some View {
              NavigationStack {
                  Form {
                      Section {
                          TextField("Title", text: $title)
                          TextField("Description", text: $description, axis: .vertical)
                        		.lineLimit(5)
                      }
                      Section("Dates") {
                          MultiDatePicker("Activities Dates", selection: $activityDates)
                      }
                  }
                  .navigationTitle("New Activity")
                  .toolbar {
                      Button("Save") {}
                  }
              }
          }
      }
    • 17:23 - Multiline text fields with line limit range

      struct ContentView: View {
          @State private var activityDates: Set<DateComponents> = [
              DateComponents(calendar: .current, year: 2022, month: 6, day: 6),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 9),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 10)
          ]
          @State private var title: String = .init()
          @State private var description: String = """
                      Join us, and let's force unwrap SwiftUl's
                      birthday presents. Note that although
                      this activity is optional, we may have
                      guards at the entry.
                      """
      
          var body: some View {
              NavigationStack {
                  Form {
                      Section {
                          TextField("Title", text: $title)
                          TextField("Description", text: $description, axis: .vertical)
                        		.lineLimit(5...10)
                      }
                      Section("Dates") {
                          MultiDatePicker("Activities Dates", selection: $activityDates)
                      }
                  }
                  .navigationTitle("New Activity")
                  .toolbar {
                      Button("Save") {}
                  }
              }
          }
      }
    • 17:40 - MultiDatePicker

      struct ContentView: View {
          @State private var activityDates: Set<DateComponents> = [
              DateComponents(calendar: .current, year: 2022, month: 6, day: 6),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 9),
              DateComponents(calendar: .current, year: 2022, month: 6, day: 10)
          ]
          @State private var title: String = .init()
          @State private var description: String = """
                      Join us, and let's force unwrap SwiftUl's
                      birthday presents. Note that although
                      this activity is optional, we may have
                      guards at the entry.
                      """
      
          var body: some View {
              NavigationStack {
                  Form {
                      Section {
                          TextField("Title", text: $title)
                          TextField("Description", text: $description, axis: .vertical)
                      }
                      Section("Dates") {
                          MultiDatePicker("Activities Dates", selection: $activityDates)
                      }
                  }
                  .navigationTitle("New Activity")
                  .toolbar {
                      Button("Save") {}
                  }
              }
          }
      }
    • 18:10 - Mixed-state toggles & pickers

      struct ContentView: View {
          enum Theme: String, CaseIterable, Identifiable {
              var id: String { self.rawValue }
              case blue, gold, black, white
      
              var swatch: some View {
                  Circle()
                      .fill(color)
                      .overlay {
                          Circle().stroke(.tertiary)
                      }
                      .frame(width: 15, height: 15)
              }
      
              var color: Color {
                  switch self {
                  case .blue: return .blue
                  case .gold: return .yellow
                  case .black: return .black
                  case .white: return .white
                  }
              }
          }
      
          enum ColorScheme: String {
              case light, dark
          }
      
          enum Decoration: String, CaseIterable {
              case balloon, confetti, inflatables, noisemakers, all, none
          }
      
          private let location = Location(
              firstLine: "One Apple Park Way", secondLine: "Cupertino, CA 95014")
      
          @State private var date: Date = DateComponents(
              calendar: .current, timeZone: .current, year: 2022, month: 6, day: 6
          ).date!
          @State private var eventDescription: String =
              "Come and join us celebrate SwiftUI's birthday party!\n🎉🎂"
      
          @State private var scheme: ColorScheme = .light
          @State private var accent: Theme = .blue
          @State private var extraGuests = false
          @State private var spacesCount: Float = 2
      
          @State private var includeBalloons = false
          @State private var includeConfetti = false
          @State private var includeInflatables = false
          @State private var includeBlowers = false
      
          @State private var selectedDecorations: [Decoration] = []
          @State private var decorationThemes: [Decoration: Theme] = [
              .balloon : .blue,
              .confetti: .gold,
              .inflatables: .black,
              .noisemakers: .white,
              .none: .black
          ]
      
          private var themes: [Binding<Theme>] {
              if selectedDecorations.count == 0 {
                  return [Binding($decorationThemes[.none])!]
              }
              return selectedDecorations.compactMap {
                  Binding($decorationThemes[$0])
              }
          }
      
          var body: some View {
              Form {
                  Section {
                      LabeledContent("Location") {
                          AddressView(location)
                      }
                      DatePicker("Date", selection: $date)
                      TextField("Description", text: $eventDescription, axis: .vertical)
                          .lineLimit(3, reservesSpace: true)
                  }
      
                  Section("Vibe") {
                      Picker("Accent color", selection: $accent) {
                          ForEach(Theme.allCases) { accent in
                              Text(accent.rawValue.capitalized).tag(accent)
                          }
                      }
                      Picker("Color scheme", selection: $scheme) {
                          Text("Light").tag(ColorScheme.light)
                          Text("Dark").tag(ColorScheme.dark)
                      }
                      #if os(macOS)
                      .pickerStyle(.inline)
                      #endif
                      Toggle(isOn: $extraGuests) {
                          Text("Allow extra guests")
                          Text("The more the merrier!")
                      }
                      if extraGuests {
                          Stepper("Guests limit", value: $spacesCount, format: .number)
                      }
                  }
      
                  Section("Decorations") {
                      Section {
                          List(selection: $selectedDecorations) {
                              DisclosureGroup {
                                  HStack {
                                      Toggle("Balloons 🎈", isOn: $includeBalloons)
                                      Spacer()
                                      decorationThemes[.balloon].map { $0.swatch }
                                  }
                                  .tag(Decoration.balloon)
      
                                  HStack {
                                      Toggle("Confetti 🎊", isOn: $includeConfetti)
                                      Spacer()
                                      decorationThemes[.confetti].map { $0.swatch }
                                  }
                                  .tag(Decoration.confetti)
      
                                  HStack {
                                      Toggle("Inflatables 🪅", isOn: $includeInflatables)
                                      Spacer()
                                      decorationThemes[.inflatables].map { $0.swatch }
                                  }
                                  .tag(Decoration.inflatables)
      
                                  HStack {
                                      Toggle("Party Horns 🥳", isOn: $includeBlowers)
                                      Spacer()
                                      decorationThemes[.noisemakers].map { $0.swatch }
                                  }
                                  .tag(Decoration.noisemakers)
                              } label: {
                                  Toggle("All Decorations", isOn: [
                                      $includeBalloons, $includeConfetti,
                                      $includeInflatables, $includeBlowers
                                  ])
                                  .tag(Decoration.all)
                              }
                              #if os(macOS)
                              .toggleStyle(.checkbox)
                              #endif
                          }
      
                          Picker("Decoration theme", selection: themes) {
                              Text("Blue").tag(Theme.blue)
                              Text("Black").tag(Theme.black)
                              Text("Gold").tag(Theme.gold)
                              Text("White").tag(Theme.white)
                          }
                          #if os(macOS)
                          .pickerStyle(.radioGroup)
                          #endif
                      }
                  }
      
              }
              .formStyle(.grouped)
          }
      }
      
      
      struct AddressView: View {
          private let location: Location
      
          init(_ location: Location) {
              self.location = location
          }
      
          var body: some View {
              VStack {
                  Text(location.firstLine)
                  Text(location.secondLine)
              }
          }
      }
      
      struct Location {
          let firstLine: String
          let secondLine: String
      }
    • 18:53 - ButtonStyle composition & Steppers

      struct ContentView: View {
          enum Theme: String, CaseIterable, Identifiable {
              var id: String { self.rawValue }
              case blue, gold, black, white
      
              var swatch: some View {
                  Circle()
                      .fill(color)
                      .overlay {
                          Circle().stroke(.tertiary)
                      }
                      .frame(width: 15, height: 15)
              }
      
              var color: Color {
                  switch self {
                  case .blue: return .blue
                  case .gold: return .yellow
                  case .black: return .black
                  case .white: return .white
                  }
              }
          }
      
          enum ColorScheme: String {
              case light, dark
          }
      
          enum Decoration: String, CaseIterable {
              case balloon, confetti, inflatables, noisemakers, all, none
          }
      
          private let location = Location(
              firstLine: "One Apple Park Way", secondLine: "Cupertino, CA 95014")
      
          @State private var date: Date = DateComponents(
              calendar: .current, timeZone: .current, year: 2022, month: 6, day: 6
          ).date!
          @State private var eventDescription: String =
              "Come and join us celebrate SwiftUI's birthday party!\n🎉🎂"
      
          @State private var scheme: ColorScheme = .light
          @State private var accent: Theme = .blue
          @State private var extraGuests = false
          @State private var spacesCount: Float = 2
      
          @State private var includeBalloons = false
          @State private var includeConfetti = false
          @State private var includeInflatables = false
          @State private var includeBlowers = false
      
          @State private var swiftastic = false
          @State private var wwdcParty = true
          @State private var offTheCharts = true
          @State private var oneMoreThing = false
      
          @State private var selectedDecorations: [Decoration] = []
          @State private var decorationThemes: [Decoration: Theme] = [
              .balloon : .blue,
              .confetti: .gold,
              .inflatables: .black,
              .noisemakers: .white,
              .none: .black
          ]
      
          private var themes: [Binding<Theme>] {
              if selectedDecorations.count == 0 {
                  return [Binding($decorationThemes[.none])!]
              }
              return selectedDecorations.compactMap {
                  Binding($decorationThemes[$0])
              }
          }
      
          var body: some View {
              Form {
                  Section {
                      LabeledContent("Location") {
                          AddressView(location)
                      }
                      DatePicker("Date", selection: $date)
                      TextField("Description", text: $eventDescription, axis: .vertical)
                          .lineLimit(3, reservesSpace: true)
                  }
      
                  Section("Vibe") {
                      Picker("Accent color", selection: $accent) {
                          ForEach(Theme.allCases) { accent in
                              Text(accent.rawValue.capitalized).tag(accent)
                          }
                      }
                      Picker("Color scheme", selection: $scheme) {
                          Text("Light").tag(ColorScheme.light)
                          Text("Dark").tag(ColorScheme.dark)
                      }
                      #if os(macOS)
                      .pickerStyle(.inline)
                      #endif
                      Toggle(isOn: $extraGuests) {
                          Text("Allow extra guests")
                          Text("The more the merrier!")
                      }
                      if extraGuests {
                          Stepper("Guests limit", value: $spacesCount, format: .number)
                      }
                  }
      
                  Section("Decorations") {
                      Section {
                          List {
                              DisclosureGroup {
                                  HStack {
                                      Toggle("Balloons 🎈", isOn: $includeBalloons)
                                      Spacer()
                                      decorationThemes[.balloon].map { $0.swatch }
                                  }
                                  .tag(Decoration.balloon)
      
                                  HStack {
                                      Toggle("Confetti 🎊", isOn: $includeConfetti)
                                      Spacer()
                                      decorationThemes[.confetti].map { $0.swatch }
                                  }
                                  .tag(Decoration.confetti)
      
                                  HStack {
                                      Toggle("Inflatables 🪅", isOn: $includeInflatables)
                                      Spacer()
                                      decorationThemes[.inflatables].map { $0.swatch }
                                  }
                                  .tag(Decoration.inflatables)
      
                                  HStack {
                                      Toggle("Party Horns 🥳", isOn: $includeBlowers)
                                      Spacer()
                                      decorationThemes[.noisemakers].map { $0.swatch }
                                  }
                                  .tag(Decoration.noisemakers)
                              } label: {
                                  Toggle("All Decorations", isOn: [
                                      $includeBalloons, $includeConfetti,
                                      $includeInflatables, $includeBlowers
                                  ])
                                  .tag(Decoration.all)
                              }
                              #if os(macOS)
                              .toggleStyle(.checkbox)
                              #endif
                          }
      
                          Picker("Decoration theme", selection: themes) {
                              Text("Blue").tag(Theme.blue)
                              Text("Black").tag(Theme.black)
                              Text("Gold").tag(Theme.gold)
                              Text("White").tag(Theme.white)
                          }
                          #if os(macOS)
                          .pickerStyle(.radioGroup)
                          #endif
                      }
                  }
      
                  Section("Hashtags") {
                      VStack(alignment: .leading) {
                          HStack {
                              Toggle("#Swiftastic", isOn: $swiftastic)
                              Toggle("#WWParty", isOn: $wwdcParty)
                          }
                          HStack {
                              Toggle("#OffTheCharts", isOn: $offTheCharts)
                              Toggle("#OneMoreThing", isOn: $oneMoreThing)
                          }
                      }
                      .toggleStyle(.button)
                      .buttonStyle(.bordered)
                  }
      
              }
              .formStyle(.grouped)
          }
      }
      
      struct AddressView: View {
          private let location: Location
      
          init(_ location: Location) {
              self.location = location
          }
      
          var body: some View {
              VStack {
                  Text(location.firstLine)
                  Text(location.secondLine)
              }
          }
      }
      
      struct Location {
          let firstLine: String
          let secondLine: String
      }
    • 19:33 - Accessibility Quick Actions

      struct ContentView: View {
          @State private var isInCart: Bool = false
      
          var body: some View {
              VStack(alignment: .leading) {
                  ItemDescriptionView()
                  addToCartButton
              }
              .accessibilityQuickAction(style: .prompt) {
                  addToCartButton
              }
          }
      
          var addToCartButton: some View {
              Button(isInCart ? "Remove from cart" : "Add to cart") {
                  isInCart.toggle()
              }
          }
      }
      
      struct ItemDescriptionView: View {
          var body: some View {
              ScrollView {
                  VStack {
                      HStack {
                          Text("🎈")
                              .font(.title2)
                          Text("Balloons")
                              .font(.title3)
                          Spacer()
                      }
                      .padding(.bottom, 4)
                      Text(
                          """
                          This is perhaps our funniest product! It is made up of a
                          rubber fabric and comes in various unique colors.
                          """)
                      .font(.caption)
                  }
              }
          }
      }
    • 20:20 - Tables on iPadOS

      struct ContentView: View {
          @StateObject private var attendeeStore = AttendeeStore()
          var body: some View {
              NavigationStack {
                  Table(attendeeStore.attendees) {
                      TableColumn("Name") { attendee in
                          AttendeeRow(attendee)
                      }
                      TableColumn("City", value: \.city)
                      TableColumn("Status") { attendee in
                          StatusRow(attendee)
                      }
                  }
                  .navigationTitle("Invitations")
                  .toolbar(id: "toolbar") {
                      ToolbarItem(id: "new", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("New Invitation", systemImage: "envelope")
                          }
                      }
                      ToolbarItem(id: "edit", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Edit", systemImage: "pencil.circle")
                          }
                      }
                      ToolbarItem(id: "share", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Share", systemImage: "square.and.arrow.up")
                          }
                      }
                      ToolbarItem(id: "tag", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Tags", systemImage: "tag")
                          }
                      }
                      ToolbarItem(
                          id: "reminder", placement: .secondaryAction, showsByDefault: false
                      ) {
                          Button(action: {}) {
                              Label("Set reminder", systemImage: "bell")
                          }
                      }
                  }
                  .toolbarRole(.editor)
              }
          }
      }
      
      class AttendeeStore: ObservableObject {
          @Published var attendees: [Attendee] = [/* Default attendees */]
      }
      
      
      struct Attendee: Identifiable, Hashable {
          enum Status: String {
              case accepted, declined, maybe
      
              func displayText() -> Text {
                  switch self {
                  case .accepted: return Text(
                      "Accepted \(Image(systemName: "person.crop.circle.badge.checkmark"))")
                  case .maybe: return Text(
                      "Maybe \(Image(systemName: "person.crop.circle.badge.questionmark"))")
                  case .declined: return Text(
                      "Declined \(Image(systemName: "person.crop.circle.badge.minus"))")
                  }
              }
          }
          
          let id = UUID()
          let memojiName: String
          let name: String
          let city: String
          let status: Status
      
          init(memojiName: String, name: String, cities: String, status: Status) {
              self.memojiName = memojiName
              self.name = name
              self.city = cities
              self.status = status
          }
      }
      
      struct AttendeeRow: View {
          let attendee: Attendee
      
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              HStack {
                  Image(attendee.memojiName)
                      .resizable()
                      .aspectRatio(contentMode: .fill)
                      #if os(macOS)
                      .frame(width: 20, height: 20)
                      .overlay {
                          Circle()
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #else
                      .frame(width: 32, height: 32)
                      .overlay {
                          RoundedRectangle(cornerRadius: 6)
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #endif
                  Text(attendee.name)
              }
          }
      }
      
      struct StatusRow: View {
          let attendee: Attendee
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              attendee.status.displayText()
                  .symbolVariant(.fill)
                  .symbolRenderingMode(.multicolor)
          }
      }
    • 21:12 - Context Menu

      struct ContentView: View {
          @StateObject private var attendeeStore = AttendeeStore()
          @State private var selection = Set<Attendee.ID>()
      
          var body: some View {
              NavigationStack {
                  Table(attendeeStore.attendees, selection: $selection) {
                      TableColumn("Name") { attendee in
                          AttendeeRow(attendee)
                      }
                      TableColumn("City", value: \.city)
                      TableColumn("Status") { attendee in
                          StatusRow(attendee)
                      }
                  }
                  .navigationTitle("Invitations")
                  #if os(macOS)
                  .contextMenu(forSelectionType: Attendee.ID.self) { selection in
                      if selection.isEmpty {
                          Button("New Invitation") { addInvitation() }
                      } else if selection.count == 1 {
                          Button("Mark as VIP") { markVIPs(selection) }
                      } else {
                          Button("Mark as VIPs") { markVIPs(selection) }
                      }
                  }
                  #endif
                  .toolbar(id: "toolbar") {
                      ToolbarItem(id: "new", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("New Invitation", systemImage: "envelope")
                          }
                      }
                      ToolbarItem(id: "edit", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Edit", systemImage: "pencil.circle")
                          }
                      }
                      ToolbarItem(id: "share", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Share", systemImage: "square.and.arrow.up")
                          }
                      }
                      ToolbarItem(id: "tag", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Tags", systemImage: "tag")
                          }
                      }
                      ToolbarItem(
                          id: "reminder", placement: .secondaryAction, showsByDefault: false
                      ) {
                          Button(action: {}) {
                              Label("Set reminder", systemImage: "bell")
                          }
                      }
                  }
                  .toolbarRole(.editor)
              }
          }
      
          private func addInvitation() {}
      
          private func markVIPs(_ items: Set<String>) {}
      }
      
      
      class AttendeeStore: ObservableObject {
          @Published var attendees: [Attendee] = [/* Default attendees */]
      }
      
      
      struct Attendee: Identifiable, Hashable {
          enum Status: String {
              case accepted, declined, maybe
      
              func displayText() -> Text {
                  switch self {
                  case .accepted: return Text(
                      "Accepted \(Image(systemName: "person.crop.circle.badge.checkmark"))")
                  case .maybe: return Text(
                      "Maybe \(Image(systemName: "person.crop.circle.badge.questionmark"))")
                  case .declined: return Text(
                      "Declined \(Image(systemName: "person.crop.circle.badge.minus"))")
                  }
              }
          }
      
          let id = UUID()
          let memojiName: String
          let name: String
          let city: String
          let status: Status
      
          init(memojiName: String, name: String, cities: String, status: Status) {
              self.memojiName = memojiName
              self.name = name
              self.city = cities
              self.status = status
          }
      }
      
      struct AttendeeRow: View {
          let attendee: Attendee
      
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              HStack {
                  Image(attendee.memojiName)
                      .resizable()
                      .aspectRatio(contentMode: .fill)
                      #if os(macOS)
                      .frame(width: 20, height: 20)
                      .overlay {
                          Circle()
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #else
                      .frame(width: 32, height: 32)
                      .overlay {
                          RoundedRectangle(cornerRadius: 6)
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #endif
                  Text(attendee.name)
              }
          }
      }
      
      struct StatusRow: View {
          let attendee: Attendee
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              attendee.status.displayText()
                  .symbolVariant(.fill)
                  .symbolRenderingMode(.multicolor)
          }
      }
    • 22:12 - Customizable toolbars

      struct ContentView: View {
          @StateObject private var attendeeStore = AttendeeStore()
          @State private var selection = Set<Attendee.ID>()
      
          var body: some View {
              NavigationStack {
                  Table(attendeeStore.attendees, selection: $selection) {
                      TableColumn("Name") { attendee in
                          AttendeeRow(attendee)
                      }
                      TableColumn("City", value: \.city)
                      TableColumn("Status") { attendee in
                          StatusRow(attendee)
                      }
                  }
                  .navigationTitle("Invitations")
                  #if os(macOS)
                  .contextMenu(forSelectionType: Attendee.ID.self) { selection in
                      if selection.isEmpty {
                          Button("New Invitation") { addInvitation() }
                      } else if selection.count == 1 {
                          Button("Mark as VIP") { markVIPs(selection) }
                      } else {
                          Button("Mark as VIPs") { markVIPs(selection) }
                      }
                  }
                  #endif
                  .toolbar(id: "toolbar") {
                      ToolbarItem(id: "new", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("New Invitation", systemImage: "envelope")
                          }
                      }
                      ToolbarItem(id: "edit", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Edit", systemImage: "pencil.circle")
                          }
                      }
                      ToolbarItem(id: "share", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Share", systemImage: "square.and.arrow.up")
                          }
                      }
                      ToolbarItem(id: "tag", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Tags", systemImage: "tag")
                          }
                      }
                      ToolbarItem(
                          id: "reminder", placement: .secondaryAction, showsByDefault: false
                      ) {
                          Button(action: {}) {
                              Label("Set reminder", systemImage: "bell")
                          }
                      }
                  }
                  .toolbarRole(.editor)
              }
          }
      
          private func addInvitation() {}
      
          private func markVIPs(_ items: Set<String>) {}
      }
      
      
      class AttendeeStore: ObservableObject {
          @Published var attendees: [Attendee] = [/* Default attendees */]
      }
      
      
      struct Attendee: Identifiable, Hashable {
          enum Status: String {
              case accepted, declined, maybe
      
              func displayText() -> Text {
                  switch self {
                  case .accepted: return Text(
                      "Accepted \(Image(systemName: "person.crop.circle.badge.checkmark"))")
                  case .maybe: return Text(
                      "Maybe \(Image(systemName: "person.crop.circle.badge.questionmark"))")
                  case .declined: return Text(
                      "Declined \(Image(systemName: "person.crop.circle.badge.minus"))")
                  }
              }
          }
      
          let id = UUID()
          let memojiName: String
          let name: String
          let city: String
          let status: Status
      
          init(memojiName: String, name: String, cities: String, status: Status) {
              self.memojiName = memojiName
              self.name = name
              self.city = cities
              self.status = status
          }
      }
      
      struct AttendeeRow: View {
          let attendee: Attendee
      
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              HStack {
                  Image(attendee.memojiName)
                      .resizable()
                      .aspectRatio(contentMode: .fill)
                      #if os(macOS)
                      .frame(width: 20, height: 20)
                      .overlay {
                          Circle()
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #else
                      .frame(width: 32, height: 32)
                      .overlay {
                          RoundedRectangle(cornerRadius: 6)
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #endif
                  Text(attendee.name)
              }
          }
      }
      
      struct StatusRow: View {
          let attendee: Attendee
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              attendee.status.displayText()
                  .symbolVariant(.fill)
                  .symbolRenderingMode(.multicolor)
          }
      }
    • 23:17 - Search Tokens

      struct ContentView: View {
          public struct AttendeeToken: Identifiable, Equatable, Hashable {
              enum Guts {
                  case name
                  case location
                  case status
              }
      
              let guts: Guts
              var query: String = .init()
      
              var id: String {
                  self.systemImage
              }
      
              static let allCases: [AttendeeToken] = [.name, .location, .status]
      
              mutating func displayName(_ query: String) -> String {
                  self.query = query
                  switch guts {
                  case .name: return "Name contains: \(query)"
                  case .location: return "City contains: \(query)"
                  case .status: return "Status contains: \(query)"
                  }
              }
      
              var systemImage: String {
                  switch guts {
                  case .name: return "person"
                  case .location: return "location.square"
                  case .status: return "person.crop.circle.badge"
                  }
              }
      
              static let name: AttendeeToken = .init(guts: .name)
              static let location: AttendeeToken = .init(guts: .location)
              static let status: AttendeeToken = .init(guts: .status)
          }
      
          @StateObject private var attendeeStore = AttendeeStore()
          @State private var selection = Set<Attendee.ID>()
      
          @State private var tokens: [AttendeeToken] = .init()
          @State private var query: String = .init()
      
          var body: some View {
              NavigationStack {
                  Table(attendeeStore.attendees, selection: $selection) {
                      TableColumn("Name") { attendee in
                          AttendeeRow(attendee)
                      }
                      TableColumn("City", value: \.city)
                      TableColumn("Status") { attendee in
                          StatusRow(attendee)
                      }
                  }
                  .navigationTitle("Invitations")
                  #if os(macOS)
                  .contextMenu(forSelectionType: Attendee.ID.self) { selection in
                      if selection.isEmpty {
                          Button("New Invitation") { addInvitation() }
                      } else if selection.count == 1 {
                          Button("Mark as VIP") { markVIPs(selection) }
                      } else {
                          Button("Mark as VIPs") { markVIPs(selection) }
                      }
                  }
                  #endif
                  .searchable(text: $query, tokens: $tokens) { token in
                      Label(token.query, systemImage: token.systemImage)
                  } suggestions: {
                      suggestions
                  }
                  .toolbar(id: "toolbar") {
                      ToolbarItem(id: "new", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("New Invitation", systemImage: "envelope")
                          }
                      }
                      ToolbarItem(id: "edit", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Edit", systemImage: "pencil.circle")
                          }
                      }
                      ToolbarItem(id: "share", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Share", systemImage: "square.and.arrow.up")
                          }
                      }
                      ToolbarItem(id: "tag", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Tags", systemImage: "tag")
                          }
                      }
                      ToolbarItem(
                          id: "reminder", placement: .secondaryAction, showsByDefault: false
                      ) {
                          Button(action: {}) {
                              Label("Set reminder", systemImage: "bell")
                          }
                      }
                  }
                  .toolbarRole(.editor)
              }
          }
      
          @ViewBuilder
          private var suggestions: some View {
              ForEach(attendeeStore.attendees) {
                  Text($0.name)
                      .foregroundColor(.black)
              }
      
              if !query.isEmpty {
                  ForEach(AttendeeToken.allCases) { token in
                      var _token = token
                      Label(_token.displayName(query), systemImage: _token.systemImage)
                          .searchCompletion(_token)
                  }
              }
          }
      
          private func addInvitation() {}
      
          private func markVIPs(_ items: Set<String>) {}
      }
      
      class AttendeeStore: ObservableObject {
          @Published var attendees: [Attendee] = [/* Default attendees */]
      }
      
      struct Attendee: Identifiable, Hashable {
          enum Status: String {
              case accepted, declined, maybe
      
              func displayText() -> Text {
                  switch self {
                  case .accepted: return Text(
                      "Accepted \(Image(systemName: "person.crop.circle.badge.checkmark"))")
                  case .maybe: return Text(
                      "Maybe \(Image(systemName: "person.crop.circle.badge.questionmark"))")
                  case .declined: return Text(
                      "Declined \(Image(systemName: "person.crop.circle.badge.minus"))")
                  }
              }
          }
      
          let id = UUID()
          let memojiName: String
          let name: String
          let city: String
          let status: Status
      
          init(memojiName: String, name: String, cities: String, status: Status) {
              self.memojiName = memojiName
              self.name = name
              self.city = cities
              self.status = status
          }
      }
      
      struct AttendeeRow: View {
          let attendee: Attendee
      
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              HStack {
                  Image(attendee.memojiName)
                      .resizable()
                      .aspectRatio(contentMode: .fill)
                      #if os(macOS)
                      .frame(width: 20, height: 20)
                      .overlay {
                          Circle()
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #else
                      .frame(width: 32, height: 32)
                      .overlay {
                          RoundedRectangle(cornerRadius: 6)
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #endif
                  Text(attendee.name)
              }
          }
      }
      
      struct StatusRow: View {
          let attendee: Attendee
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              attendee.status.displayText()
                  .symbolVariant(.fill)
                  .symbolRenderingMode(.multicolor)
          }
      }
    • 23:28 - Search scopes

      struct ContentView: View {
          enum AttendanceScope {
              case inPerson
              case online
          }
      
          public struct AttendeeToken: Identifiable, Equatable, Hashable {
              enum Guts {
                  case name
                  case location
                  case status
              }
      
              let guts: Guts
              var query: String = .init()
      
              var id: String {
                  self.systemImage
              }
      
              static let allCases: [AttendeeToken] = [.name, .location, .status]
      
              mutating func displayName(_ query: String) -> String {
                  self.query = query
                  switch guts {
                  case .name: return "Name contains: \(query)"
                  case .location: return "City contains: \(query)"
                  case .status: return "Status contains: \(query)"
                  }
              }
      
              var systemImage: String {
                  switch guts {
                  case .name: return "person"
                  case .location: return "location.square"
                  case .status: return "person.crop.circle.badge"
                  }
              }
      
              static let name: AttendeeToken = .init(guts: .name)
              static let location: AttendeeToken = .init(guts: .location)
              static let status: AttendeeToken = .init(guts: .status)
          }
      
          @StateObject private var attendeeStore = AttendeeStore()
          @State private var selection = Set<Attendee.ID>()
      
          @State private var tokens: [AttendeeToken] = .init()
          @State private var query: String = .init()
          @State private var scope: AttendanceScope = .inPerson
      
          var body: some View {
              NavigationStack {
                  Table(attendeeStore.attendees, selection: $selection) {
                      TableColumn("Name") { attendee in
                          AttendeeRow(attendee)
                      }
                      TableColumn("City", value: \.city)
                      TableColumn("Status") { attendee in
                          StatusRow(attendee)
                      }
                  }
                  .navigationTitle("Invitations")
                  #if os(macOS)
                  .contextMenu(forSelectionType: Attendee.ID.self) { selection in
                      if selection.isEmpty {
                          Button("New Invitation") { addInvitation() }
                      } else if selection.count == 1 {
                          Button("Mark as VIP") { markVIPs(selection) }
                      } else {
                          Button("Mark as VIPs") { markVIPs(selection) }
                      }
                  }
                  #endif
                  .searchable(
                      text: $query, tokens: $tokens, scope: $scope
                  ) { token in
                      Label(
                          token.query,
                          systemImage: token.systemImage)
                  } scopes: {
                      Text("In Person").tag(AttendanceScope.inPerson)
                      Text("Online").tag(AttendanceScope.online)
                  } suggestions: {
                      suggestions
                  }
                  .toolbar(id: "toolbar") {
                      ToolbarItem(id: "new", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("New Invitation", systemImage: "envelope")
                          }
                      }
                      ToolbarItem(id: "edit", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Edit", systemImage: "pencil.circle")
                          }
                      }
                      ToolbarItem(id: "share", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Share", systemImage: "square.and.arrow.up")
                          }
                      }
                      ToolbarItem(id: "tag", placement: .secondaryAction) {
                          Button(action: {}) {
                              Label("Tags", systemImage: "tag")
                          }
                      }
                      ToolbarItem(
                          id: "reminder", placement: .secondaryAction, showsByDefault: false
                      ) {
                          Button(action: {}) {
                              Label("Set reminder", systemImage: "bell")
                          }
                      }
                  }
                  .toolbarRole(.editor)
              }
          }
      
          @ViewBuilder
          private var suggestions: some View {
              ForEach(attendeeStore.attendees) {
                  Text($0.name)
                      .foregroundColor(.black)
              }
      
              if !query.isEmpty {
                  ForEach(AttendeeToken.allCases) { token in
                      var _token = token
                      Label(_token.displayName(query), systemImage: _token.systemImage)
                          .searchCompletion(_token)
                  }
              }
          }
      
          private func addInvitation() {}
      
          private func markVIPs(_ items: Set<String>) {}
      }
      
      class AttendeeStore: ObservableObject {
          @Published var attendees: [Attendee] = [/* Default attendees */]
      }
      
      
      struct Attendee: Identifiable, Hashable {
          enum Status: String {
              case accepted, declined, maybe
      
              func displayText() -> Text {
                  switch self {
                  case .accepted: return Text(
                      "Accepted \(Image(systemName: "person.crop.circle.badge.checkmark"))")
                  case .maybe: return Text(
                      "Maybe \(Image(systemName: "person.crop.circle.badge.questionmark"))")
                  case .declined: return Text(
                      "Declined \(Image(systemName: "person.crop.circle.badge.minus"))")
                  }
              }
          }
      
          let id = UUID()
          let memojiName: String
          let name: String
          let city: String
          let status: Status
      
          init(memojiName: String, name: String, cities: String, status: Status) {
              self.memojiName = memojiName
              self.name = name
              self.city = cities
              self.status = status
          }
      }
      
      struct AttendeeRow: View {
          let attendee: Attendee
      
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              HStack {
                  Image(attendee.memojiName)
                      .resizable()
                      .aspectRatio(contentMode: .fill)
                      #if os(macOS)
                      .frame(width: 20, height: 20)
                      .overlay {
                          Circle()
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #else
                      .frame(width: 32, height: 32)
                      .overlay {
                          RoundedRectangle(cornerRadius: 6)
                              .stroke(Color.gray.opacity(0.2), lineWidth: 1)
                      }
                      #endif
                  Text(attendee.name)
              }
          }
      }
      
      struct StatusRow: View {
          let attendee: Attendee
          init(_ attendee: Attendee) {
              self.attendee = attendee
          }
      
          var body: some View {
              attendee.status.displayText()
                  .symbolVariant(.fill)
                  .symbolRenderingMode(.multicolor)
          }
      }
    • 24:45 - PhotosPicker

      import PhotosUI
      import CoreTransferable
      
      struct ContentView: View {
          @ObservedObject var viewModel: FilterModel = .shared
          
          var body: some View {
              NavigationStack {
                  Gallery()
                      .navigationTitle("Birthday Filter")
                      .toolbar {
                          PhotosPicker(
                              selection: $viewModel.imageSelection,
                              matching: .images
                          ) {
                              Label("Pick a photo", systemImage: "plus.app")
                          }
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                      }
              }
          }
      }
      
      struct Gallery: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              VStack {
                  switch viewModel.imageState {
                  case .success(let image):
                      image
                          .resizable()
                          .aspectRatio(contentMode: .fill)
                          .draggable(image)
                  case .loading:
                      ProgressView()
                  case .empty:
                      Text("No Photo \(Image(systemName: "photo"))")
                          .font(.title2)
                          .fontWeight(.semibold)
                      Text("Drag and drop a photo or press\n \(Image(systemName: "plus.app")) to choose a photo manually.")
                          .foregroundColor(.secondary)
                          .multilineTextAlignment(.center)
                  case .failure:
                      Image(systemName: "exclamationmark.triangle.fill")
                          .font(.system(size: 40))
                          .foregroundColor(.white)
                  }
              }
              .padding()
          }
      }
      
      @MainActor
      class FilterModel: ObservableObject {
          static let shared = FilterModel()
      
          enum ImageState {
              case empty, loading(Progress), success(Image), failure(Error)
          }
      
          @Published private(set) var processedImage: Image?
          @Published var imageState: ImageState = .empty
          @Published var imageSelection: PhotosPickerItem? = nil {
              didSet {
                  if let imageSelection = imageSelection {
                      let progress = loadTransferable(from: imageSelection)
                      imageState = .loading(progress)
                  } else {
                      imageState = .empty
                  }
              }
          }
      
          func applyFilter() { /* Apply your filter */ }
      
          private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
              return imageSelection.loadTransferable(type: Image.self) { result in
                  DispatchQueue.main.async {
                      guard imageSelection == self.imageSelection else { return }
                      switch result {
                      case .success(let image?):
                          self.imageState = .success(image)
                      case .success(nil):
                          self.imageState = .empty
                      case .failure(let error):
                          self.imageState = .failure(error)
                      }
                  }
              }
          }
      }
    • 25:51 - ShareLink

      import PhotosUI
      import CoreTransferable
      
      struct ContentView: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              NavigationStack {
                  Gallery()
                      .navigationTitle("Birthday Filter")
                      .toolbar {
                          PhotosPicker(
                              selection: $viewModel.imageSelection,
                              matching: .images
                          ) {
                              Label("Pick a photo", systemImage: "plus.app")
                          }
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                          if let item = viewModel.processedImage {
                              ShareLink(
                                  item: item, preview: SharePreview("Birthday Effects"))
                          }
                      }
              }
          }
      }
      
      struct Gallery: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              VStack {
                  switch viewModel.imageState {
                  case .success(let image):
                      image
                          .resizable()
                          .aspectRatio(contentMode: .fill)
                          .draggable(image)
                  case .loading:
                      ProgressView()
                  case .empty:
                      Text("No Photo \(Image(systemName: "photo"))")
                          .font(.title2)
                          .fontWeight(.semibold)
                      Text("Drag and drop a photo or press\n \(Image(systemName: "plus.app")) to choose a photo manually.")
                          .foregroundColor(.secondary)
                          .multilineTextAlignment(.center)
                  case .failure:
                      Image(systemName: "exclamationmark.triangle.fill")
                          .font(.system(size: 40))
                          .foregroundColor(.white)
                  }
              }
              .padding()
          }
      }
      
      @MainActor
      class FilterModel: ObservableObject {
          static let shared = FilterModel()
      
          enum ImageState {
              case empty, loading(Progress), success(Image), failure(Error)
          }
      
          @Published private(set) var processedImage: Image?
          @Published var imageState: ImageState = .empty
          @Published var imageSelection: PhotosPickerItem? = nil {
              didSet {
                  if let imageSelection = imageSelection {
                      let progress = loadTransferable(from: imageSelection)
                      imageState = .loading(progress)
                  } else {
                      imageState = .empty
                  }
              }
          }
      
          func applyFilter() { /* Apply your filter */}
      
          private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
              return imageSelection.loadTransferable(type: Image.self) { result in
                  DispatchQueue.main.async {
                      guard imageSelection == self.imageSelection else { return }
                      switch result {
                      case .success(let image?):
                          self.imageState = .success(image)
                      case .success(nil):
                          self.imageState = .empty
                      case .failure(let error):
                          self.imageState = .failure(error)
                      }
                  }
              }
          }
      }
    • 26:17 - Context Menu

      import PhotosUI
      import CoreTransferable
      
      struct ContentView: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              NavigationStack {
                  Gallery()
                      .navigationTitle("Birthday Filter")
                      .toolbar {
                          PhotosPicker(
                              selection: $viewModel.imageSelection,
                              matching: .images
                          ) {
                              Label("Pick a photo", systemImage: "plus.app")
                          }
                          if let item = viewModel.processedImage {
                              ShareLink(
                                  item: item, preview: SharePreview("Birthday Effects"))
                          }
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                      }
                      .contextMenu {
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                          if let item = viewModel.processedImage {
                              ShareLink(
                                  item: item, preview: SharePreview("Birthday Effects"))
                          }
                          Button(role: .destructive) {
                              viewModel.deleteCurrentPhoto()
                          } label: {
                              Label("Delete", systemImage: "trash")
                          }
                      }
              }
          }
      }
      
      struct Gallery: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              VStack {
                  switch viewModel.imageState {
                  case .success(let image):
                      image
                          .resizable()
                          .aspectRatio(contentMode: .fill)
                          .draggable(image)
                  case .loading:
                      ProgressView()
                  case .empty:
                      Text("No Photo \(Image(systemName: "photo"))")
                          .font(.title2)
                          .fontWeight(.semibold)
                      Text("Drag and drop a photo or press\n \(Image(systemName: "plus.app")) to choose a photo manually.")
                          .foregroundColor(.secondary)
                          .multilineTextAlignment(.center)
                  case .failure:
                      Image(systemName: "exclamationmark.triangle.fill")
                          .font(.system(size: 40))
                          .foregroundColor(.white)
                  }
              }
              .padding()
          }
      }
      
      @MainActor
      class FilterModel: ObservableObject {
          static let shared = FilterModel()
      
          enum ImageState {
              case empty, loading(Progress), success(Image), failure(Error)
          }
      
          @Published private(set) var processedImage: Image?
          @Published var imageState: ImageState = .empty
          @Published var imageSelection: PhotosPickerItem? = nil {
              didSet {
                  if let imageSelection = imageSelection {
                      let progress = loadTransferable(from: imageSelection)
                      imageState = .loading(progress)
                  } else {
                      imageState = .empty
                  }
              }
          }
      
          func applyFilter() { /* Apply your filter */}
      
          func deleteCurrentPhoto() {}
      
          private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
              return imageSelection.loadTransferable(type: Image.self) { result in
                  DispatchQueue.main.async {
                      guard imageSelection == self.imageSelection else { return }
                      switch result {
                      case .success(let image?):
                          self.imageState = .success(image)
                      case .success(nil):
                          self.imageState = .empty
                      case .failure(let error):
                          self.imageState = .failure(error)
                      }
                  }
              }
          }
      }
    • 26:50 - Drop destination

      import PhotosUI
      import CoreTransferable
      
      struct ContentView: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              NavigationStack {
                  Gallery()
                      .navigationTitle("Birthday Filter")
                      .toolbar {
                          PhotosPicker(
                              selection: $viewModel.imageSelection,
                              matching: .images
                          ) {
                              Label("Pick a photo", systemImage: "plus.app")
                          }
                          if let item = viewModel.processedImage {
                              ShareLink(
                                  item: item, preview: SharePreview("Birthday Effects"))
                          }
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                      }
                      .contextMenu {
                          Button {
                              viewModel.applyFilter()
                          } label: {
                              Label("Apply Filter", systemImage: "camera.filters")
                          }
                          if let item = viewModel.processedImage {
                              ShareLink(
                                  item: item, preview: SharePreview("Birthday Effects"))
                          }
                          Button(role: .destructive) {
                              viewModel.deleteCurrentPhoto()
                          } label: {
                              Label("Delete", systemImage: "trash")
                          }
                      }
                      .dropDestination(payloadType: Image.self) { receivedImages, location in
                          guard let image = receivedImages.first else {
                              return false
                          }
                          viewModel.imageState = .success(image)
                          return true
                      }
              }
          }
      }
      
      struct Gallery: View {
          @ObservedObject var viewModel: FilterModel = .shared
      
          var body: some View {
              VStack {
                  switch viewModel.imageState {
                  case .success(let image):
                      image
                          .resizable()
                          .aspectRatio(contentMode: .fill)
                          .draggable(image)
                  case .loading:
                      ProgressView()
                  case .empty:
                      Text("No Photo \(Image(systemName: "photo"))")
                          .font(.title2)
                          .fontWeight(.semibold)
                      Text("Drag and drop a photo or press\n \(Image(systemName: "plus.app")) to choose a photo manually.")
                          .foregroundColor(.secondary)
                          .multilineTextAlignment(.center)
                  case .failure:
                      Image(systemName: "exclamationmark.triangle.fill")
                          .font(.system(size: 40))
                          .foregroundColor(.white)
                  }
              }
              .padding()
          }
      }
      
      @MainActor
      class FilterModel: ObservableObject {
          static let shared = FilterModel()
      
          enum ImageState {
              case empty, loading(Progress), success(Image), failure(Error)
          }
      
          @Published private(set) var processedImage: Image?
          @Published var imageState: ImageState = .empty
          @Published var imageSelection: PhotosPickerItem? = nil {
              didSet {
                  if let imageSelection = imageSelection {
                      let progress = loadTransferable(from: imageSelection)
                      imageState = .loading(progress)
                  } else {
                      imageState = .empty
                  }
              }
          }
      
          func applyFilter() { /* Apply your filter */}
      
          func deleteCurrentPhoto() {}
      
          private func loadTransferable(from imageSelection: PhotosPickerItem) -> Progress {
              return imageSelection.loadTransferable(type: Image.self) { result in
                  DispatchQueue.main.async {
                      guard imageSelection == self.imageSelection else { return }
                      switch result {
                      case .success(let image?):
                          self.imageState = .success(image)
                      case .success(nil):
                          self.imageState = .empty
                      case .failure(let error):
                          self.imageState = .failure(error)
                      }
                  }
              }
          }
      }
    • 28:15 - Shape Styles: CalendarIcon

      struct CalendarIcon: View {
          var body: some View {
              VStack {
                  Image(systemName: "calendar")
                      .font(.system(size: 80, weight: .medium))
                  Text("June 6")
              }
              .background(in: Circle().inset(by: -20))
              .backgroundStyle(
                  .blue
                  .gradient
              )
              .foregroundStyle(.white.shadow(.drop(radius: 1, y: 1.5)))
              .padding(20)
          }
      }
    • 28:49 - Shape Styles: Icon Grid

      struct Icon: View {
          let systemSymbolName: String
          let color: Color
          let shadow: ShadowStyle
          var foregroundColor: Color = .white
      
          var body: some View {
              VStack {
                  Image(systemName: systemSymbolName)
                      .resizable()
                      .aspectRatio(1.0, contentMode: .fit)
                      .padding(2)
              }
              .background(in: Circle().inset(by: -20))
              .backgroundStyle(
                  color
                  .gradient
              )
              .foregroundStyle(foregroundColor.shadow(shadow))
              .padding(20)
          }
      }
      
      private let dropStyle = ShadowStyle.drop(radius: 1, y: 1.5)
      private let innerStyle = ShadowStyle.inner(radius: 1.5)
      
      let icons: [Icon]  = [
          Icon(systemSymbolName: "person", color: .red, shadow: dropStyle),
          Icon(systemSymbolName: "basketball", color: .orange, shadow: dropStyle),
          Icon(systemSymbolName: "globe.central.south.asia", color: .yellow, shadow: innerStyle),
          Icon(systemSymbolName: "carrot", color: .green, shadow: innerStyle, foregroundColor: .orange),
          Icon(systemSymbolName: "sailboat", color: .mint, shadow: innerStyle),
          Icon(systemSymbolName: "figure.open.water.swim", color: .teal, shadow: dropStyle),
          Icon(systemSymbolName: "ladybug.fill", color: .cyan, shadow: innerStyle),
          Icon(systemSymbolName: "calendar", color: .blue, shadow: dropStyle),
          Icon(systemSymbolName: "moon.stars", color: .indigo, shadow: dropStyle),
          Icon(systemSymbolName: "brain.head.profile", color: .purple, shadow: innerStyle),
          Icon(systemSymbolName: "birthday.cake", color: .pink, shadow: dropStyle),
          Icon(systemSymbolName: "house.circle.fill", color: .white, shadow: dropStyle),
          Icon(systemSymbolName: "lizard", color: .brown, shadow: dropStyle),
          Icon(systemSymbolName: "flag.checkered", color: .black, shadow: dropStyle),
          Icon(systemSymbolName: "character.book.closed", color: .gray, shadow: dropStyle),
      ]
      
      struct IconGrid: View {
          var body: some View {
              Grid(horizontalSpacing: 16, verticalSpacing: 16) {
                  ForEach(0..<3) { i in
                      GridRow {
                          ForEach(0..<5) { j in
                              icons[i * 5 + j]
                          }
                      }
                  }
              }
              .background(.black.opacity(0.8))
          }
      }
    • 29:07 - Graphics: Dancing symbol grid

      // MARK: - Dancing Symbol Grid
      
      struct SymbolSquare: View {
          let color: Color
          let imageName: String
          var image: some View {
              Image(systemName: imageName)
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .padding()
                  .frame(maxWidth: .infinity, maxHeight: .infinity)
          }
      
          var body: some View {
              image
                  .background {
                      RoundedRectangle(cornerRadius: 6, style: .continuous)
                          .fill(
                              .ellipticalGradient(
                                  color
                                      .gradient
                              )
                          )
                  }
          }
      }
      
      /// If `true`, the party will commence. 
      private let startTheParty = false
      
      private let partySymbols = ["party.popper", "balloon", "balloon.2", "birthday.cake"]
      
      struct DancingSymbolSquare: View {
          let color: Color
          let imageName: String
        
          /// Allows staggered dancing — doesn't look quite as nice.
          let seed: Int
          private let timer = Timer.publish(every: 0.234378662, on: .main, in: .default)
          @State private var cancellable: Cancellable? = nil
          @State private var heavy = false
          @State var fontSize = 20 as CGFloat
      
          var body: some View {
              SymbolSquare(color: color, imageName: imageName)
                  .font(.body.weight(heavy ? .black : .thin))
                  .onReceive(timer) { date in
                      if heavy {
                          withAnimation(.easeOut(duration: 0.468757324 - 0.1)) {
                              heavy.toggle()
                          }
                      } else {
                          withAnimation(.easeIn(duration: 0.1)) {
                              heavy.toggle()
                          }
                      }
                  }
                  .onAppear {
                      if startTheParty {
                          DispatchQueue.main.asyncAfter(deadline: .now()  + Double(seed) * 0.25) {
                              cancellable = timer.connect()
                          }
                      }
                  }
                  .drawingGroup(opaque: true)
          }
      }
      
      struct SymbolGrid: View {
          var body: some View {
              Grid {
                  GridRow {
                      DancingSymbolSquare(color: .yellow, imageName:partySymbols[0], seed: 0)
                      DancingSymbolSquare(color: .green, imageName: partySymbols[1], seed: 0)
                  }
      
                  GridRow {
                      DancingSymbolSquare(color: .indigo, imageName: partySymbols[2], seed: 0)
                      DancingSymbolSquare(color: .purple, imageName: partySymbols[3],  seed: 0)
                  }
              }
              .frame(maxWidth: .infinity, maxHeight: .infinity)
          }
      }
    • 30:15 - Graphics: Text transitions

      struct TextTransitionsView: View {
          @State private var expandMessage = true
          private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2)))
          private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))
      
          var body: some View {
              Text("Happy Birthday SwiftUI!")
                  .font(expandMessage ? .largeTitle.weight(.heavy) : .body)
                  .foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)
                  .onTapGesture { withAnimation { expandMessage.toggle() }}
                  .frame(maxWidth: expandMessage ? 160 : 250)
                  .drawingGroup()
                  .padding(20)
                  .background(.pink.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))
          }
      }
    • 31:16 - Layout: Grid

      struct VIPDetailView: View {
          var body: some View {
              Grid {
                  GridRow {
                      NameHeadline()
                          .gridCellColumns(2)
                  }
                  GridRow {
                      CalendarIcon()
                      SymbolGrid()
                  }
              }
              .frame(width: 300, height: 300)
          }
      }
      
      struct NameHeadline: View {
          var body: some View {
              HStack {
                  Color.green.background(in: RoundedRectangle(cornerRadius: 8))
                      .frame(maxWidth: .infinity, maxHeight: .infinity)
                  VStack(alignment: .leading) {
                      Text("Franck Ndame Mpouli")
                          .font(.title2)
                          .foregroundStyle(.shadow(.drop(radius: 2, y: 3)))
                      Text("Party Planning Committee").bold()
                  }
              }
              .padding()
              .frame(maxWidth: .infinity, maxHeight: .infinity)
              .background(
                  .white.gradient,
                  in: RoundedRectangle(cornerRadius: 12, style: .continuous)
              )
          }
      }
      
      struct CalendarIcon: View {
          var body: some View {
              VStack {
                  Image(systemName: "calendar")
                      .font(.system(size: 80, weight: .medium))
                  Text("June 6")
              }
              .background(in: Circle().inset(by: -20))
              .backgroundStyle(
                  .blue
                  .gradient
              )
              .foregroundStyle(.white.shadow(dropStyle))
              .padding(20)
              .frame(maxWidth: .infinity, maxHeight: .infinity)
          }
      }
    • 32:04 - Layout: Seating Chart Layout

      // MARK: Custom Table Layout
      
      private let tableSize = CGSize(width: 130, height: 90)
      private let guestSize = CGSize(width: 40, height: 40)
      
      /// Which of 6 tables this view represents
      private struct TableViewLayoutKey: LayoutValueKey {
          static let defaultValue: Int? = nil
      }
      
      extension View {
          fileprivate func tableViewLayoutKey(_ value: Int) -> some View  {
              return layoutValue(key: TableViewLayoutKey.self, value: value)
          }
      }
      
      /// Which of 36 guests this view represents
      private struct GuestViewLayoutKey: LayoutValueKey {
          static let defaultValue: Int? = 0
      }
      
      extension View {
      
          /// Guests 1 - 36
          fileprivate func guestViewLayoutKey(_ value: Int) -> some View  {
              return layoutValue(key: GuestViewLayoutKey.self, value: value)
          }
      }
      
      let initials = [
      "Ju",
      "As",
      "Ma",
      "As",
      "Ly",
      "Ga",
      "Ni",
      "Ar",
      "Ca",
      "Do",
      "Je",
      "Ca",
      "Em",
      "Ma",
      "Ze",
      "Jo",
      "Da",
      "Sh",
      "Sa",
      "Pl",
      "Pa",
      "Sc",
      "Ma",
      "Je",
      "Li",
      "Ma",
      "Ta",
      "Je",
      "Cu",
      "Lu",
      "Ra",
      "Na",
      "Sa",
      "Pa",
      "Le",
      "Pi",
      ]
      
      struct SeatingChartView: View {
      
          /// If true, the guests will be positioned in "pods" of tables. No table will touch another table. Otherwise
          /// the guests will side in two longs rows.
          @State private var usePods = true
      
          var body: some View {
              ZStack(alignment: .bottomTrailing) {
                  GeometryReader { proxy in
                      SeatingLayout(usePods: usePods).callAsFunction {
                          TableView(tableNumber: 1)
                          TableView(tableNumber: 2)
                          TableView(tableNumber: 3)
                          TableView(tableNumber: 4)
                          TableView(tableNumber: 5)
                          TableView(tableNumber: 6)
                          ForEach(1..<37) { i in
                              SeatedGuestOption2(guestNumber: i - 1)
                          }
                      }
                      .animation(.default, value: proxy.size)
                  }
                  .background(.black.opacity(0.13))
                  Picker("Arrangement", selection: $usePods.animation()) {
                      Text("Pods").tag(true)
                      Text("Rows  ").tag(false)
                  }
                  .fixedSize()
                  .pickerStyle(.segmented)
                  .padding()
              }
          }
      }
      
      /// heh.
      struct TableView: View {
          let tableNumber: Int
      
          var body: some View {
              ZStack(alignment: .bottomTrailing) {
                  HStack {
                      Image(systemName: "table.furniture")
                          .background(.quaternary.shadow(.inner(radius: 1, y: 1.5)),
                                      in: Circle().inset(by: -8))
                          .padding(5)
                      Text("Table \(tableNumber)")
                  }
                  .foregroundStyle(.secondary)
                  .padding(8)
                  .frame(width: tableSize.width, height: tableSize.height)
                  #if os(macOS) || os(iOS)
                  .background(.regularMaterial.shadow(.drop(radius: 1, y: 1.5)),
                              in: RoundedRectangle(cornerRadius: 12, style: .continuous))
                  #endif
              }
      
              .tableViewLayoutKey(tableNumber)
          }
      }
      
      private let colors: [Color] = [
          .red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue,
          .indigo, .purple, .pink, .gray, .black, .white, .brown,
          .red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue,
          .indigo, .purple, .pink, .gray, .black, .white, .brown, .red,
          .orange, .yellow, .green, .mint, .teal, .cyan
      ]
      
      struct SeatedGuest: View {
      
          let guestNumber: Int
      
          var body: some View {
              Image(systemName: "person")
                  .resizable()
                  .aspectRatio(contentMode: .fit)
                  .padding(9)
                  .background(in: Circle())
                  .backgroundStyle(
                      colors[guestNumber].gradient
                  )
                  .foregroundStyle(guestNumber == 13 ? .black : .white)
                  .frame(width: 40, height: 40)
                  .guestViewLayoutKey(guestNumber + 1)
          }
      }
      
      struct SeatedGuestOption2: View {
          let guestNumber: Int
      
          var body: some View {
              Circle()
                  .stroke(colors[guestNumber], style: StrokeStyle(lineWidth: 3))
                  .background(.white.gradient, in: Circle())
                  .frame(width: guestSize.width, height: guestSize.height)
                  .guestViewLayoutKey(guestNumber + 1)
                  .overlay {
                      Text(initials[guestNumber])
                          .foregroundColor(.secondary)
                          .font(.callout)
      
                  }
          }
      }
      
      struct SeatingChartView_Previews: PreviewProvider {
          static var previews: some View {
              SeatingChartView()
                  .frame(width: 600, height: 600)
          }
      }
      
      struct SeatingLayout: Layout {
      
          /// If true, the guests will be positioned in "pods" of tables. No table will touch another table. Otherwise
          /// the guests will side in two longs rows.
          let usePods: Bool
      
          struct Cache {
              ///  The width proposed to the view. We assume a certain height, otherwise, overlapping views
              var width: CGFloat?
          }
      
          func sizeThatFits(
              proposal: ProposedViewSize,
              subviews: LayoutSubviews,
              cache: inout Cache
          ) -> CGSize {
              cache.width = proposal.width
              return proposal.replacingUnspecifiedDimensions()
          }
      
          func makeCache(subviews: Subviews) -> Cache { Cache() }
      
          func placeSubviews(in bounds: CGRect,
                             proposal: ProposedViewSize,
                             subviews: Subviews,
                             cache: inout Cache) {
              guard let width = cache.width else { return }
      
              /// Helper function: Place 6 guests around all edges of a table.
              func seat(_ guests: [LayoutSubview], around table: CGRect) {
                  guests[0].place(
                      at: .init(
                          x: table.origin.x + 3 - guestSize.width,
                          y: table.origin.y + (table.height / 2.0) - (guestSize.height / 2.0)),
                      proposal: .infinity)
                  guests[1].place(
                      at: .init(
                          x: table.origin.x + (table.width / 4.0) - guestSize.width / 2.0,
                          y: table.origin.y + 5 - guestSize.height),
                      proposal: .infinity)
                  guests[2].place(
                      at: .init(
                          x: table.origin.x + table.width * 0.75 - guestSize.width / 2.0,
                          y: table.origin.y + 5 - guestSize.height),
                      proposal: .infinity)
                  guests[3].place(
                      at: .init(
                          x: table.maxX - 5,
                          y: table.origin.y + (table.height / 2.0) - (guestSize.height / 2.0)),
                      proposal: .infinity)
                  guests[4].place(
                      at: .init(
                          x: table.origin.x + table.width * 0.75 - guestSize.width / 2.0,
                          y: table.maxY - 5),
                      proposal: .infinity)
                  guests[5].place(
                      at: .init(
                          x: table.origin.x + (table.width / 4.0) - guestSize.width / 2.0,
                          y: table.maxY - 5),
                      proposal: .infinity)
              }
      
              /// Helper function: Place 6 guests, dining hall style (not along the shorter sides of a table)
              func seat(_ guests: [LayoutSubview], along table: CGRect) {
                  guests[0].place(
                      at: .init(
                          x: table.minX + tableSize.width / 3 - guestSize.width - 4,
                          y: table.origin.y + 5 - guestSize.height),
                      proposal: .infinity)
                  guests[1].place(
                      at: .init(
                          x: table.minX + tableSize.width * 2/3 - guestSize.width - 4,
                          y: table.origin.y + 5 - guestSize.height),
                      proposal: .infinity)
                  guests[2].place(
                      at: .init(
                          x: table.minX + tableSize.width - guestSize.width - 4,
                          y: table.origin.y + 5 - guestSize.height),
                      proposal: .infinity)
                  guests[3].place(
                      at: .init(
                          x: table.minX + tableSize.width / 3 - guestSize.width - 4,
                          y: table.maxY - 5),
                      proposal: .infinity)
                  guests[4].place(
                      at: .init(
                          x: table.minX + tableSize.width * 2/3 - guestSize.width - 4,
                          y: table.maxY - 5),
                      proposal: .infinity)
                  guests[5].place(
                      at: .init(
                          x: table.minX + tableSize.width - guestSize.width - 4,
                          y: table.maxY - 5),
                      proposal: .infinity)
              }
      
              // Get tables
              let table1 = subviews.first(where: { $0[TableViewLayoutKey.self] == 1 })!
              let table2 = subviews.first(where: { $0[TableViewLayoutKey.self] == 2 })!
              let table3 = subviews.first(where: { $0[TableViewLayoutKey.self] == 3 })!
              let table4 = subviews.first(where: { $0[TableViewLayoutKey.self] == 4 })!
              let table5 = subviews.first(where: { $0[TableViewLayoutKey.self] == 5 })!
              let table6 = subviews.first(where: { $0[TableViewLayoutKey.self] == 6 })!
      
              // Get guests
              let table1Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 1 && guestNumber <= 6
                  }
              let table2Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 7 && guestNumber <= 12
                  }
              let table3Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 13 && guestNumber <= 18
                  }
              let table4Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 19 && guestNumber <= 24
                  }
              let table5Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 25 && guestNumber <= 30
                  }
              let table6Guests = subviews
                  .filter {
                      guard let guestNumber = $0[GuestViewLayoutKey.self] else { return false }
                      return guestNumber >= 31 && guestNumber <= 36
                  }
      
              if usePods {
                  let table1Origin = CGPoint(x: 60, y: 120)
                  let table2Origin = CGPoint(x: 200, y: 280)
                  let table3Origin = CGPoint(x: 50, y: 450)
                  let table4Origin = CGPoint(x: 300, y: 120)
                  let table5Origin = CGPoint(x: 440, y: 280)
                  let table6Origin = CGPoint(x: 290, y: 450)
                  table1.place(at: table1Origin, proposal: .infinity)
                  table2.place(at: table2Origin, proposal: .infinity)
                  table3.place(at: table3Origin, proposal: .infinity)
                  table4.place(at: table4Origin, proposal: .infinity)
                  table5.place(at: table5Origin, proposal: .infinity)
                  table6.place(at: table6Origin, proposal: .infinity)
                  seat(table1Guests, around: CGRect(origin: table1Origin, size: tableSize))
                  seat(table2Guests, around: CGRect(origin: table2Origin , size: tableSize))
                  seat(table3Guests, around: CGRect(origin: table3Origin, size: tableSize))
                  seat(table4Guests, around: CGRect(origin: table4Origin, size: tableSize))
                  seat(table5Guests, around: CGRect(origin: table5Origin , size: tableSize))
                  seat(table6Guests, around: CGRect(origin: table6Origin, size: tableSize))
              } else {
                  let table1Origin = CGPoint(x: width / 2.0 - 6 - tableSize.width * 1.5, y: 130)
                  let table2Origin = CGPoint(x: table1Origin.x + tableSize.width + 6, y: 130)
                  let table3Origin = CGPoint(x: table2Origin.x + tableSize.width + 6, y: 130)
                  let table4Origin = CGPoint(x: width / 2.0 - 6 - tableSize.width * 1.5, y: 360)
                  let table5Origin = CGPoint(x: table1Origin.x + tableSize.width + 6, y: 360)
                  let table6Origin = CGPoint(x: table2Origin.x + tableSize.width + 6, y: 360)
                  table1.place(at: table1Origin, proposal: .infinity)
                  table2.place(at: table2Origin, proposal: .infinity)
                  table3.place(at: table3Origin, proposal: .infinity)
                  table4.place(at: table4Origin, proposal: .infinity)
                  table5.place(at: table5Origin, proposal: .infinity)
                  table6.place(at: table6Origin, proposal: .infinity)
                  seat(table1Guests, along: CGRect(origin: table1Origin, size: tableSize))
                  seat(table2Guests, along: CGRect(origin: table2Origin , size: tableSize))
                  seat(table3Guests, along: CGRect(origin: table3Origin, size: tableSize))
                  seat(table4Guests, along: CGRect(origin: table4Origin, size: tableSize))
                  seat(table5Guests, along: CGRect(origin: table5Origin , size: tableSize))
                  seat(table6Guests, along: CGRect(origin: table6Origin, size: tableSize))
              }
          }
      }
    • 32:50 - AnyLayout invitation

      import SwiftUI
      import GameplayKit
      import Combine
      
      @main
      struct InvitationApp: App {
          var body: some Scene {
              WindowGroup {
                  PolygonDesignerView()
                      .environmentObject(PolygonModel())
                  #if os(iOS)
                      .statusBar(hidden: true)
                  #endif
                      .edgesIgnoringSafeArea(.all)
              }
          }
      }
      
      // MARK: Views
      
      /// A view that arranges polygons in a grid, or a custom, scattered layout.
      private struct DynamicPolygonView: View {
          @EnvironmentObject var model: PolygonModel
          @Binding var cycleLayouts: Bool
      
          private var sideLength: Int {
              Int(CGFloat(model.polygonGeometries.count).squareRoot())
          }
      
          /// Timer whose ticking dictates how often to regenerate and animate-to a new scattered layout.
          /// - Note: The layout will only transition if `cycleLayouts` is `true`.
          private let layoutChangingTimer = Timer
              .publish(every: 1.2, on: .current, in: .default).autoconnect()
      
          /// Animation used to transition layouts
          private let animation = Animation.easeInOut(duration: 1.3)
      
          /// Timer that ticks at 128 beats per minute, matching the beat of the song in the WWDC session.
          let musicBeatTimer = Timer
              .publish(every: 0.234378662, tolerance: 0,  on: .main, in: .default)
      
          @State private var musicBeatTimerCancellable: (any Cancellable)? = nil
      
          /// Whether or not the font should be rendered heavy.
          @State private var heavy: Bool = false
      
          @State private var scatteredLayout = newScatteredLayout(
              Date(timeIntervalSince1970: 0)
          )
      
          /// By providing a seed value, the `ScatteredLayout` struct will know when to bust its cache and
          /// generate new layout data.
          private static func newScatteredLayout(_ seed: Date) -> ScatteredLayout {
              ScatteredLayout(count: PolygonModel.total,
                              seed: seed.timeIntervalSinceReferenceDate,
                              textAvoidanceRect: CGRect(
                                  x: 152,
                                  y: 245,
                                  width: 220,
                                  height: 40)
              )
          }
      
          var body: some View {
              let layout = model.usesGridLayout
              ? AnyLayout(Grid(alignment: .center,
                               horizontalSpacing: 0,
                               verticalSpacing: 0))
              : AnyLayout(scatteredLayout)
      
              ZStack(alignment: .center) {
                  Label(title:  {
                      Text("You're Invited")
                  }, icon: { Image(systemName: "party.popper.fill")})
                  .font(.system(size:100).weight(heavy ? .black : .thin))
                  .onTapGesture {
                      musicBeatTimerCancellable = musicBeatTimer.connect()
                  }
                  .zIndex(-1)
      
                  layout {
                      ForEach((0..<sideLength), id: \.self) { row in
                          GridRow { // GridRow is a no-op in non-Grid layouts
                              ForEach((0..<sideLength), id: \.self) { column in
                                  let polygon = model
                                      .polygonGeometries[sideLength * row + column]
                                  PolygonView(polygonGeometry: polygon)
                                      .polygonViewLayoutKey(polygon)
                              }
                          }
                      }
                  }
              }
              .drawingGroup()
              .frame(maxWidth: .infinity, maxHeight: .infinity)
              .onReceive(musicBeatTimer) { date in
                  if heavy {
                      // Transitioning to a thin font happens slowly
                      withAnimation(.easeOut(duration: 0.468757324 - 0.1)) {
                          heavy.toggle()
                      }
                  } else {
                      // Transitioning to thick happens quickly, to give the
                      // appearance of a "strong" downbeat
                      withAnimation(.easeIn(duration: 0.1)) {
                          heavy.toggle()
                      }
                  }
              }
              .onReceive(layoutChangingTimer) { date in
                  guard cycleLayouts else { return }
                  withAnimation(animation) {
                      scatteredLayout = DynamicPolygonView.newScatteredLayout(date)
                  }
      
              }
          }
      }
      
      private struct PolygonDesignerView: View {
          @EnvironmentObject var model: PolygonModel
          @State var cycleLayouts = false
          @State var hideDesignerView = true
      
          var body: some View {
              ZStack(alignment: .bottom) {
                  DynamicPolygonView(cycleLayouts: $cycleLayouts)
                      .onTapGesture(count: 2) {
                          withAnimation {
                              hideDesignerView.toggle()
                          }
                      }
                  ControlView(cycleLayouts: $cycleLayouts)
                      .padding()
                      .background(.thickMaterial)
                      .offset(CGSize(width: 0, height: hideDesignerView ? 300 : 0))
              }
          }
      }
      
      /// Tunes the parameters of a `PolygonModel`
      private struct ControlView: View {
      
          /// The instance `self` tunes the parameters of.
          @EnvironmentObject var model: PolygonModel
      
          /// Can be used by a parent view to cycle through instances of layouts.
          @Binding var cycleLayouts: Bool
      
          var body: some View {
              VStack {
                  Button("Reset", action: model.reset)
                  let layout = HStack()
                  layout {
                      Toggle("Tiled", isOn: Binding(get: {
                          model.tiled
                      }, set: { tile in
                          // After toggled, wait 5 seconds, then transition back to a
                          // scattered layout
                          DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                              withAnimation(.linear(duration: 1.4)) {
                                  model.usesGridLayout = false
                                  model.drawAsRandomPolygons = true
                              }
                          }
      
                          withAnimation(.linear(duration: 1.8)) {
                              model.usesGridLayout = tile
                              model.drawAsRandomPolygons = !tile
                          }
                      }))
      
                      Toggle("Cycle Layouts", isOn: $cycleLayouts)
                  }
              }
              .padding(2)
          }
      }
      
      // MARK: PolygonView
      
      /// Wraps a ``Polygon`` shape applying a fill.
      private struct PolygonView: View {
          var polygonGeometry: PolygonGeometry
      
          var body: some View {
              Polygon(polygonGeometry: polygonGeometry)
                  .fill(polygonGeometry.color)
          }
      }
      
      /// A Polygon shape that supports any number of sides as defined by `polygonGeometry`
      private struct Polygon: Shape {
          var polygonGeometry: PolygonGeometry
      
          typealias AnimatableData = AnimatableVector
      
          var animatableData: AnimatableVector {
              get { polygonGeometry.vectorPath }
              set { polygonGeometry.points = newValue.points }
          }
      
          func path(in rect: CGRect) -> Path {
              // Scale up the shape's path to fill as much space as it is given
              let path = polygonGeometry.path
              let boundingRect = path.boundingRect
      
              let xScale = rect.width / boundingRect.width
              let yScale = rect.height / boundingRect.height
      
              let translate = CGAffineTransform(
                  translationX: -boundingRect.origin.x * xScale,
                  y: -boundingRect.origin.y * yScale
              )
              let scale = CGAffineTransform(scaleX: xScale, y: yScale)
              return path.applying(scale.concatenating(translate))
          }
      
          func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
              if proposal == .infinity {
                  // If proposed infinite space, use the preferred, absolute size.
                  return CGSize(width: polygonGeometry.sideLength,
                                height: polygonGeometry.sideLength)
              } else {
                  // If we don't have infinite space, assume we've been given all the
                  // space the parent view can afford, and take all of it.
                  return proposal.replacingUnspecifiedDimensions()
              }
          }
      }
      
      // MARK: ScatteredLayout
      
      private struct PolygonViewLayoutKey: LayoutValueKey {
          static let defaultValue: PolygonGeometry? = nil
      }
      
      extension View {
          fileprivate func polygonViewLayoutKey(_ value: PolygonGeometry)
          -> some View {
              return layoutValue(key: PolygonViewLayoutKey.self, value: value)
          }
      }
      
      /// ScatteredLayout assumes a certain standard size and lays out its views
      /// (tagged with `PolygonViewLayoutKey` data) such that they don't collide
      /// within that size. As the size grows, the shapes stay the same size,
      /// but get farther or closer.
      private struct ScatteredLayout: Layout {
      
          /// Cache data for a `ScatteredLayout`.
          struct Cache {
      
              /// Maps a `PolygonGeometry.id` to its position in a `standardSize`
              /// coordinate space.
              var rects: [UUID: CGRect]
      
              /// Used as a cache buster.
              var seed: TimeInterval?
          }
      
          /// The smallest size a view using this layout can be.
          private let minimumBaseSize: CGSize
      
          /// The base coordinate system this view assumes when laying out.
          private let standardSize: CGSize = CGSize(width: 500, height: 500)
      
          /// Clients can pass a value here and polygons won't be placed in that rect.
          var textAvoidanceRect: CGRect = .zero
      
          /// If different, we've been requested to bust the cache, and create a new
          /// one.
          /// - Note the cache can persist across different instances of a
          ///  `ScatteredLayout`
          private let seed: TimeInterval
      
          func sizeThatFits(
              proposal: ProposedViewSize,
              subviews: LayoutSubviews,
              cache: inout Cache
          ) -> CGSize {
              let proposedSize = proposal
                  .replacingUnspecifiedDimensions(by: minimumBaseSize)
              return CGSize(
                  width: proposedSize.width
                      .clamped(
                          to: minimumBaseSize.width..<CGFloat.greatestFiniteMagnitude
                      ),
                  height: proposedSize.height
                      .clamped(
                          to: minimumBaseSize.height..<CGFloat.greatestFiniteMagnitude
                      )
              )
          }
      
          init(count: Int, seed: TimeInterval, textAvoidanceRect: CGRect = .zero) {
              self.seed = seed
              minimumBaseSize = CGSize(width: CGFloat(count), height: CGFloat(count))
              self.textAvoidanceRect = textAvoidanceRect
          }
      
          func makeCache(subviews: Subviews) -> Cache {
              var cache: Cache =  Cache(rects: [:], seed: self.seed)
              var placedPolygons: [CGRect] = []
      
              for subview in subviews {
      
                  guard let polygon = subview[PolygonViewLayoutKey.self] else {
                      // This is the title text view, skip it.
                      continue
                  }
      
                  var subviewsPreferredSize = subview.sizeThatFits(.infinity)
                  var counter = 20
      
                  while counter > 0 {
                      counter -= 1
                      let randomX = CGFloat.random(in: 0..<standardSize.width)
                      let randomY: CGFloat
                      if randomX > textAvoidanceRect.minX
                          && randomX < textAvoidanceRect.maxX {
                          // Pick from either above or below the avoidance rect
                          if Bool.random() {
                              randomY = CGFloat.random(
                                  in: 0..<textAvoidanceRect.minY
                              )
                          } else {
                              randomY = CGFloat.random(
                                  in: textAvoidanceRect.maxY..<standardSize.height
                              )
                          }
                      } else {
                          randomY = CGFloat.random(in: 0..<standardSize.height)
                      }
      
                      let origin = CGPoint(x: randomX, y: randomY)
                      let rect = CGRect(origin: origin, size: subviewsPreferredSize)
      
                      if placedPolygons.allSatisfy({ placed in
                          !placed.intersects(rect)
                      }) && !rect.intersects(textAvoidanceRect) {
                          // The shape found a non-overlapping place to be. Lock in
                          // it's position
                          placedPolygons.append(rect)
                          cache.rects[polygon.id] =
                          CGRect(origin: origin,
                                 size: subviewsPreferredSize)
                          break
                      } else  {
                          if (counter == 0) {
                              if rect.intersects(textAvoidanceRect) {
                                  subviewsPreferredSize = .zero
                              }
                              placedPolygons.append(rect)
                              cache.rects[polygon.id] =
                              CGRect(origin: origin,
                                     size: subviewsPreferredSize)
                          }
                      }
                  }
              }
              return cache
          }
      
          func placeSubviews(in bounds: CGRect,
                             proposal: ProposedViewSize,
                             subviews: Subviews,
                             cache: inout Cache) {
              // We have the frame value cached (via makeCache())
              // for every view to be placed in a `standardSize` coordinate system.
              // Now we need to map that `standardSize` to the size was proposed.
              let proposedSize = proposal
                  .replacingUnspecifiedDimensions(by: minimumBaseSize)
              let xProposedToBaseRatio = proposedSize.width / standardSize.width
              let yProposedToBaseRatio = proposedSize.height / standardSize.height
      
              for subview in subviews {
                  guard let uuid = subview[PolygonViewLayoutKey.self]?.id, let rect =
                          cache.rects[uuid] else {
                      let desiredSize = subview.sizeThatFits(.zero)
                      let centered = desiredSize.centered(in: bounds)
                      subview.place(
                          at: centered.origin,
                          proposal: ProposedViewSize(
                              width: desiredSize.width,
                              height: desiredSize.height
                          )
                      )
                      continue
                  }
      
                  let mappedPoint = CGPoint(x: rect.origin.x * xProposedToBaseRatio,
                                            y: rect.origin.y * yProposedToBaseRatio)
      
                  subview.place(at: mappedPoint,
                                proposal: ProposedViewSize(width: rect.size.width,
                                                           height:rect.size.height)
                  )
              }
          }
      
          func updateCache(_ cache: inout Cache, subviews: Subviews) {
      
              // Bust the cache if we've been given a new seed value
              // or if our subviews have been swapped out from underneath us.
              if self.seed != cache.seed
                  || !cache.rects.contains(where: { (key: UUID, value: CGRect) in
                      subviews.first?[PolygonViewLayoutKey.self]?.id == key
                  })  {
                  cache = makeCache(subviews: subviews)
                  return
              }
          }
      
      }
      
      /// This struct facilitates animation of point-based `Path`s so long as said
      /// source and destination `Path` have an equal number of vertices.
      private struct AnimatableVector: VectorArithmetic {
      
          static var zero: AnimatableVector = AnimatableVector(points: [])
      
          private(set) var points: [CGPoint]
      
          var magnitudeSquared: Double {
              let squared = points.map { point in
                  CGPoint(x: point.x * point.x, y: point.y * point.y)
              }
              let sumOfSquares = squared.map { point in // dot product?
                  sqrt(point.x + point.y)
              }
              let sum = sumOfSquares.reduce(0, +)
              return Double(sum)
          }
      
          /// Facilitates a valid `.zero` value, no matter the dimension of the vector
          subscript(safe index: Int) -> CGPoint {
              return (self.points.count <= index) ? .zero : points[index]
          }
      
          static func - (lhs: AnimatableVector, rhs: AnimatableVector)
          -> AnimatableVector {
              let negated = rhs.points.map { CGPoint(x: -$0.x, y: -$0.y) }
              return lhs + AnimatableVector(points: negated)
          }
      
          static func + (lhs: AnimatableVector, rhs: AnimatableVector)
          -> AnimatableVector {
              var output: [CGPoint] = []
              for i in 0..<lhs.points.count {
                  output.append(CGPoint(x: lhs[safe: i].x + rhs[safe: i].x,
                                        y:lhs[safe: i].y + rhs[safe: i].y ))
              }
              return AnimatableVector(points: output)
          }
      
          mutating func scale(by rhs: Double) {
              points = points.map { CGPoint(x: $0.x * CGFloat(rhs),
                                            y: $0.y * CGFloat(rhs)) }
          }
      }
      
      // MARK: Random Polygon Generation & Geometry
      
      private let mean: Float = 10
      private let deviation: Float = 3
      private let gaussian = GKGaussianDistribution(
          randomSource: GKARC4RandomSource(),
          mean: mean,
          deviation: deviation)
      
      /// Factory type for creating points describing a random Polygon
      private struct PolygonGeometry: Identifiable, Equatable, Hashable {
      
          /// The horizontal and vertical side lengths of the polygon's bounding box.
          let sideLength: CGFloat
      
          /// A constant count of the total points that comprise this
          /// `PolygonGeometry`'s path. Clients can set `points` to a new value, but
          /// the new value should have the same `count` for smooth `Path` animations
          let numberOfVertices: Int
      
          /// Supports animation of point-based `Path`s by providing an array of
          /// points that can be interpolated.
          var vectorPath: AnimatableVector {
              AnimatableVector(points: points)
          }
      
          /// If `false`, this instance will present itself as a rectangular shape
          /// (not necessarily with 4 vertices) that fills available space.
          private(set) var drawsAsPolygon: Bool = true
      
          /// Points describing the `Path` used to render `self`.
          var points: [CGPoint] {
              willSet {
                  assert(points.count == polygonPathPoints.count)
              }
          }
      
          /// Delineate the path of the random polygon.
          private let polygonPathPoints: [CGPoint]
      
          let color: Color = [
              Color(red: 0.73, green: 0.20, blue: 0.20),
              Color(red: 0.95, green: 0.66, blue: 0.24),
              Color(red: 0.14, green: 0.29, blue: 0.49),
              Color(red: 0.46, green: 0.76, blue: 0.67),
              Color(red: 0.30, green: 0.33, blue: 0.22),
              Color(red: 0.49, green: 0.55, blue: 0.64),
              Color(red: 0.92, green: 0.53, blue: 0.30),
              Color(red: 0.20, green: 0.45, blue: 0.55),
              Color(red: 0.41, green: 0.45, blue: 0.45),
              Color(red: 0.87, green: 0.67, blue: 0.61)
          ].randomElement()!
      
          private var spikiness: CGFloat = 0.2
          private var irregularity: CGFloat = 0.2
      
          let id = UUID()
      
          /// Owning `Shape` instances should use this to draw.
          var path: Path { Path(from: points) }
      
          init(pointsVector: [CGPoint], sideLength: CGFloat) {
              self.numberOfVertices = pointsVector.count
              self.points = pointsVector
              self.polygonPathPoints = points
              self.sideLength = sideLength
          }
      
          func drawn(asRandomizedPolygon: Bool) -> Self {
              var copy = self
              copy.drawsAsPolygon = asRandomizedPolygon
              copy.points = asRandomizedPolygon
              ? copy.polygonPathPoints
              : CGRect(x: 0, y: 0, width: 1, height: 1)
                  .pointSequence(of: copy.numberOfVertices)
              return copy
          }
      
          func hash(into hasher: inout Hasher) {
              hasher.combine(id)
          }
      }
      
      /// A namespace around functionality to generate a  path drawn in a 1x1 square
      /// with configurable "irregularity" and "spikiness".
      /// The closer both are to zero, the closer the generated polygon is to a
      /// [regular polygon](https://mathworld.wolfram.com/RegularPolygon.html)
      private enum UnitPolygonGeometryFactory {
      
          /// The maximum possible radius. A value of 0.5 restricts the algorithm
          /// to the unit square.
          private static let maxRadius: CGFloat = 0.5
      
          /// A — by no means definitive — algorithm for creating an arbitrary
          /// polygon of `vertexCount` vertices
          /// - Parameters:
          ///   - vertexCount: How many vertices (and edges) the polygon will have
          ///   - irregularity: A subjective term for how "irregular" the polygon is.
          ///   A fully regular polygon has all equal sides, assuming 0 `spikinesss`.
          ///   - spikiness: A subjective term for how "spiky" the polygon is.
          ///   A polygon with high spikiness will have more vertices closer and
          ///   farther from where the vertex would be on a regular polygon.
          /// - Returns: An array of points representing the point-based path of
          /// the polygon
          static func random(vertexCount: Int,
                             irregularity: CGFloat = 0.2,
                             spikiness: CGFloat = 0.2)
          -> [CGPoint] {
      
              let floatVertices = CGFloat(vertexCount)
      
              // Irregularity is how much we're willing to allow the angular steps to
              // vary from "perfect". For example, in a regular (all sides equal)
              // six-sided polygon, each angular step is 2𝜋 / 6. Irregularity
              // defines the range that value can take, centered around a mean of
              // 2𝜋 / 6. We accept an irregularity between 0 and 1, and then
              // scale it for how much that represents out of a circle's radians.
              let scaledIrregularity = irregularity * 2.0 * CGFloat.pi / floatVertices
      
              // Spikiness describes how often we want to see values that are very
              // far from where a vertex of a regular polygon would be. For example,
              // a high positive spikiness might push a vertex radially very far from
              // the center, leading to a big "spike". Meanwhile, a spikiness of 0
              // will yield more circular polygons.
              let denormalizedSpikiness = spikiness * maxRadius
      
              let gaussian = GKGaussianDistribution(
                  randomSource: GKARC4RandomSource(),
                  mean: Float(maxRadius * 1024),
                  deviation: Float(denormalizedSpikiness * 1024))
      
              // Generate the angular steps
              var raidanAngleSteps: [CGFloat] = []
      
              // Both of these measured in radians
              let minimumSliceWidth =
              (2.0 * CGFloat.pi / floatVertices) - scaledIrregularity
              let maximumSliceWidth =
              (2.0 * CGFloat.pi / floatVertices) + scaledIrregularity
      
              var sum: CGFloat = 0
      
              for _ in (0..<vertexCount) {
                  let radians = CGFloat
                      .random(in: minimumSliceWidth...maximumSliceWidth)
                  raidanAngleSteps.append(radians)
                  sum += radians
              }
      
              // Re-divide these steps so the point 0 and n+1 are the same.
              // I.e. if the random angle generation from the above loop yielded
              // more or less than 2𝜋 radians, reapportion those divisions to sum to
              // 2𝜋.
              let k = sum / (2 * CGFloat.pi)
              (0..<vertexCount).forEach { i in
                  raidanAngleSteps[i] /= k
              }
      
              let maximumPossibleGaussianSample = CGFloat(
                  gaussian.mean + Float(denormalizedSpikiness * 1024)*3
              )
      
              // Finally, make all of the normalized points within a 1x1 square
              // Unlike the unit circle of traditional geometry, because (0, 0) is in
              // the top left, (0.5, 0.5) is in the middle. Thus, positively
              // incrementing the angle moves us clockwise around the circle
              var points: [CGPoint] = []
              let center = CGPoint(x: maxRadius, y: maxRadius)
              var cumulativeAngle: CGFloat = 0.0
              for i in (0..<Int(vertexCount)) {
      
                  // * 2 to keep the sample <= 0.5 (`maxRadius)
                  let radiusForPoint = CGFloat(gaussian.nextInt())
                  / (maximumPossibleGaussianSample * 2)
      
                  let x = center.x + radiusForPoint * cos(cumulativeAngle)
                  let y = center.y + radiusForPoint * sin(cumulativeAngle)
                  points.append(CGPoint(x: x, y: y))
      
                  cumulativeAngle += raidanAngleSteps[i]
              }
              return points
          }
      }
      
      // MARK: Observable Polygon Model
      
      /// A `PolygonModel` describes a collection of randomized ``Polygons`` that
      /// can be laid out by `AnyLayout` type.
      private class PolygonModel: ObservableObject {
      
          static let total = (maxSides - minSides + 1) * polygonsPerSideCount
      
          /// The minimum sides the randomly generated sides will have
          private static let minSides = 4
      
          /// The maximum sides the randomly generated sides will have
          private static let maxSides = 7
      
          /// The number of randomly generated polygons to make _per side length_.
          private static let polygonsPerSideCount = 32
      
          /// All `PolygonGeometry`s that are laid out with `scatteredLayout`
          @Published var polygonGeometries: [PolygonGeometry] = makeGeometries()
      
          /// If `true`, `self` is expressing a grid layout with rectangular tiles.
          var tiled: Bool { usesGridLayout && !drawAsRandomPolygons }
      
          /// If `true`, ignore `scatteredLayout` and instead use a `Grid` layout
          @Published var usesGridLayout: Bool = false
      
          /// If `true`, `polygonGeometries` draw themselves as randomized polygons.
          /// If false, a rectangle that fills all available space.
          @Published var drawAsRandomPolygons: Bool = true {
              didSet {
                  polygonGeometries = polygonGeometries.map {
                      $0.drawn(asRandomizedPolygon: drawAsRandomPolygons)
                  }
              }
          }
      
          /// Tunable by clients to experiment with different values.
          let spikiness: CGFloat = 0.2
          /// Tunable by clients to experiment with different values.
          let irregularity: CGFloat = 0.2
      
          /// Creates many ``PolygonGeometry`` instances with the given parameters.
          /// - Parameters:
          ///   - irregularity: A subjective term for how "irregular" the polygon is.
          ///   A fully regular polygon has all equal sides, assuming 0 `spikinesss`.
          ///   - spikiness: A subjective term for how "spiky" the polygon is.
          ///   A polygon with high spikiness will have more vertices closer and
          ///   farther from where the vertex would be on a regular polygon.
          /// - Returns: An array of `n` polygons where `n` is defined by the
          ///  `PolygonModel` class.
          private static func makeGeometries(
              irregularity: CGFloat = 0.3,
              spikiness: CGFloat = 0.3) -> [PolygonGeometry] {
                  var scales: Array<CGFloat> = polygonSizeRatios
                      .reduce(into: []) { partialResult, sizeRatio in
                          let (size, percentage) = sizeRatio
                          let scalesToMake = Int(ceil(percentage * CGFloat(total)))
                          partialResult.append(contentsOf: (0..<scalesToMake)
                              .map { _ in CGFloat.random(in: size.sizeRange) })
                      }.shuffled()
      
                  return (minSides...maxSides).flatMap { vertexCount in
                      return (0..<polygonsPerSideCount).map { _ in
                          let unitPolygon = UnitPolygonGeometryFactory
                              .random(vertexCount: vertexCount,
                                      irregularity: irregularity,
                                      spikiness: spikiness)
                          let polygonGeometry = PolygonGeometry(
                              pointsVector: unitPolygon,
                              sideLength: scales.removeFirst())
                          return polygonGeometry
                      }
                  }.shuffled()
              }
      
          /// Complete remove and regenerate all model data.
          func reset() {
              polygonGeometries.removeAll(keepingCapacity: true)
              polygonGeometries = PolygonModel.makeGeometries(
                  irregularity: irregularity,
                  spikiness: spikiness
              )
          }
      }
      
      private extension PolygonModel {
      
          /// Use a sampling of various sized polygons
          enum PieceSize: Hashable {
              case tiny
              case small
              case medium
              case large
      
              /// The range for the side length of the bounding rect of a polygon
              var sizeRange: ClosedRange<CGFloat> {
                  switch self {
                  case .tiny:
                      return 16.0...25.0
                  case .small:
                      return 25.0...40.0
                  case .medium:
                      return 40.0...50.0
                  case .large:
                      return 50.0...65.0
                  }
              }
          }
      
          /// This dictionary denotes the ratio of sizes to use.
          /// - warning: Should sum to 100.
          private static let polygonSizeRatios: [PieceSize: CGFloat] =
          [
              .large: 0.15,
              .medium: 0.25,
              .small: 0.25,
              .tiny: 0.35
          ]
      }
      
      // MARK: - Utility Extensions
      
      extension FloatingPoint {
      
          /// - returns an instance of `Self` clamped to the ``ClosedRange``.
          func clamped(to limits: ClosedRange<Self>) -> Self {
              return min(max(self, limits.lowerBound), limits.upperBound)
          }
      
          /// - returns an instance of `Self` clamped to the ``Range``.
          /// - note the value returned will be less than the provided upper bound, as
          ///  is dictated by ``Range``.
          func clamped(to limits: Range<Self>) -> Self {
              return min(max(self, limits.lowerBound), limits.upperBound.nextDown)
          }
      }
      
      extension CGRect {
      
          /// Creates a rectangular sequence of `vertexCount `points denoting a
          /// rectangular path.
          /// - note This is helpful for animating a `Path` composed of `vertexCount`
          /// points into a ``Rectangle``.
          func pointSequence(of vertexCount: Int) -> [CGPoint] {
              // Start at a random corner. When many Polygons are using this
              // animation at once, if they all start at the same corner, an
              // unnatural uniformity of motion emerges.
              var startingPercent = [0, 0.25, 0.5, 0.75].randomElement()!
              var points: [CGPoint] = []
      
              let extraPoints = vertexCount - 4
              let (groups, remainder) = extraPoints
                  .quotientAndRemainder(dividingBy: 3)
      
              for edge in 0...3 {
                  points.append(pointAlongPerimeter(at: startingPercent))
                  for i in (0..<(edge == 3 ? remainder : groups)) {
                      points.append(pointAlongPerimeter(
                          at: startingPercent + 0.25
                          / CGFloat(groups + 1) * CGFloat(i)))
                  }
                  startingPercent += 0.25
                  startingPercent.formTruncatingRemainder(dividingBy: 1)
              }
              assert(points.count == vertexCount)
              return points
          }
      
          /// Returns the ``CGPoint`` that is `percent` along the path of `self`,
          /// with 0% mapping to the top-left corner, progressing clockwise.
          /// E.g. 50% would map to the bottom right corner if and only if `self` is
          ///  a square.
          /// - Parameters:
          ///   - percent: A percentage between `0.0` and `1.0`
          private func pointAlongPerimeter(at percent: CGFloat) -> CGPoint {
              let perimeter = size.width * 2 + size.height * 2
      
              // Mark the four corners as percentages around the rect. For example,
              /// these values for a square would be 25%, 50%, 75%, 100%
              let topRight = size.width / perimeter
              let bottomRight = topRight + (size.height / perimeter)
              let bottomLeft = bottomRight + (size.width / perimeter)
              let topLeft = 1.0
      
              switch percent {
              case 0..<topRight:
                  return CGPoint(
                      x: percent / topRight * size.width,
                      y: minY)
              case topRight..<bottomRight:
                  return CGPoint(
                      x: maxX,
                      y: (percent - topRight)
                      / (bottomRight - topRight) * size.height)
              case bottomRight..<bottomLeft:
                  return CGPoint(
                      x: maxX - ((percent - bottomRight) / (bottomLeft - bottomRight)
                                 * size.width),
                      y: maxY)
              case bottomLeft...topLeft:
                  return CGPoint(
                      x: minX,
                      y: maxY - (percent - bottomLeft) / (topLeft - bottomLeft)
                      * size.height
                  )
              default:
                  preconditionFailure("Invalid percentage requested")
              }
          }
      }
      
      /// Returns a new `CGRect` with the same size as `self`, but centered in `other`
      /// vertically, and horizontally.
      extension CGSize {
          func centered(in other: CGRect) -> CGRect {
              CGRect(x: other.midX - width / 2.0,
                     y: other.midY - height / 2.0,
                     width: width,
                     height: height)
          }
      }
      
      extension Path {
          /// Convenience for initializing a `Path` from an array of `CGPoint`s given
          /// the first point element is the `Path`'s first point.
          init(from points: [CGPoint]) {
              self.init()
              self.addLines(points)
              self.closeSubpath()
          }
      }
  • 特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。

    クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。

Developer Footer

  • ビデオ
  • WWDC22
  • SwiftUIの最新情報
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン