ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIの新機能
SwiftUIを利用することで、iPhone、iPad、Mac、Apple Watch、Apple TV向けに、より良く、よりパワフルなAppが構築できます。アウトライン、グリッド、ツールバーなどのインターフェースの改善を含め、SwiftUIの最新機能について学びましょう。Appleでサインインなどの機能を実現するAppleフレームワーク全体に対し、強化されたSwiftUIのサポートを活用しましょう。新しいビジュアルエフェクトやコントロール、スタイルもご紹介します。また、新しいAppやScene APIを使い、SwiftUIだけでAppを作成する方法、コンプリケーションや新しいウィジェットをカスタマイズする方法もお伝えします。このセッションを有効活用するには、ある程度、SwiftUIに親しみがあることが望まれます。初めての方は、"Introduction to SwiftUI"をご覧ください。
リソース
関連ビデオ
WWDC20
- ウィジェットにおけるSwiftUIビューの構築
- Mac Catalyst Appのインターフェイスの最適化
- Mac Catalystに関する新機能
- SF Symbols 2
- Swift UIにおけるデータの重要事項
- Swiftの新機能
- SwiftUI の Stack, Grid, Outline
- SwiftUIでコンプリケーションを構築する
- SwiftUIでドキュメントベースのAppを構築する
- SwiftUIにおけるAppの重要事項
- SwiftUIのご紹介
- tvOS向けのSwiftUI Appを構築する
- UIタイポグラフィーの詳細
- Widget Code-Along 1-冒険の始まり
- Widget Code-Along 2 - 異なるタイムライン
- Widget Code-Along 3 - アドバンシングタイムライン
- WidgetKitについて
-
ダウンロード
WWDCへようこそ “SwiftUIの新機能” SwiftUIチームのマット・リケットソンです 後半では同僚のテイラーも登場します 昨年 発表したSwiftUIでは― Appleの全プラットフォームで 優れたユーザーインターフェイスの構築が可能に 今回 SwiftUIの 2回目の大きなリリースをご紹介します
今年は 一度では伝えきれないほど たくさんの新機能が追加されました なるべく多く説明しますが― より詳細なセッションについても 同時に案内していきます まずは新たなAppと ウィジェットAPIです リストやコレクションの表示に関する 改善点にも触れます ツールバーやコントロールのための マルチプラットフォームAPIや― Appをスタイリングする 視覚効果についても説明します 最後はSwiftUI Appと システムの統合について話しましょう まずはAppとウィジェットです 今回初めてSwiftUIだけで Appをビルドできます UIKit AppKit WatchKitのAppに SwiftUIコードを埋め込む必要はありません 見てみましょう これは“Hello, world!”という SwiftUI Appです “Hello, world!” 見てのとおり100%機能するAppを― コードをビルドして実行できます とても簡潔で― App全体が 140文字に収まっています でも惑わされてはいけません SwiftUIは宣言型Appのために 知的で自動的かつカスタマイズ可能な動作を― シンプルで柔軟なAPIに搭載しています Book Clubで読んだ本を 管理するAppを書きました メインのUIを表すため 下にはカスタムビューを書き― 上ではメインウィンドウのコンテンツとして そのビューを使っています これら2つの宣言が 類似しているのが分かるでしょうか SwiftUIの新しいApp APIは― ビューコードで すでに使用した宣言や ステートドリブンパターンをフォローします
どちらも定義はプロトコルに準拠した構造です プロパティを使い データの依存関係も宣言できます そのデータをボディのプロパティ内で使えば― Appとビューの双方のため 宣言的なUIコンテンツを定義できます
1つ 大きく違うのは― Appのボディプロパティの リターンタイプです Appのボディはシーンを返します これはAppのUIを象徴する SwiftUIの新コンセプトで― プラットフォームで個別に表示します シーンの説明とAppやビューとの 関係性は別のトークで詳しく話します 次に“WindowGroup”という App内のシーンに注目しましょう これを見れば SwiftUI内のシーンが ボックスの外で― 知的でマルチプラットフォームな機能を 提供できることが分かります
WindowGroupはiOS Appでは― 単一の全画面ウィンドウを作成し 管理します このコードはwatchOSでも実行でき― 同様に単一の全画面ウィンドウを管理します watchOSとiOSでは Appの見た目は違います しかし どちらのプラットフォームでも コアの構造は同じで― 1つのApp用の宣言を 共有しているのです 実際 この私のAppは tvOSやiPadでも使えます iPadOSはマルチウィンドウAppを サポートするので― 追加機能をフリーで取得します 複数のインスタンスを 並べて表示することなどが可能になります
これは複数のウィンドウでもサポートできる macOSにも適用されています “コマンド”と“N”のショートカットキーで 新規ウィンドウを複数作成し― 単一のタブ付きウィンドウに 集めることができます
SwiftUIでは新規ウィンドウメニューコマンドの メインメニューへの自動的な追加も可能です いずれも簡単なAppの宣言で 実現できます WindowGroup APIを使い インターフェイスを定義したおかげです
SwiftUIは 他のタイプのシーンもサポートし― 複雑なAppをビルドするため ビューのように一緒に構築できます 例えば macOSで利用可能な 新しい“Settings”シーンは― MacのAppに 設定ウィンドウを追加します これはAppのメニューの 標準コマンドを自動的に設定し― ウィンドウが正しいスタイルになるよう 処理してくれるシーンです
SwiftUIのシーンAPIは― ドキュメントベースのAppも サポートします 私がビルドした 描画用のAppも対象です
今年 登場した この“DocumentGroup”シーンは― ドキュメントベースのシーンの編集や保存を 自動的に管理してくれます こちらはiOSやiPadOS macOSでサポートされています DocumentGroupが iOSとiPadOSで ドキュメントブラウザを自動で表示するのは― 他のメインインターフェイスが 提供されていない場合です Macでは新規ドキュメントごとに 異なるウィンドウが開かれ― 一般的なアクションが選べるコマンドを メインメニューに自動的に追加します
メニューコマンドといえば SwiftUIでは― 新しいコマンドモディファイアを使用し コマンドを追加することもできます ここでは新しい形を作るための “カスタムシェイプメニュー”を追加しました macOSはカスタムメニューをメインメニューの 適切なセクションに自動的に追加し― “keyboardShortcut”ビューモディファイアを使い 新規ショートカットキーが何かを表示します
コマンドAPIには他にも多くの機能があります ユーザーの視点をベースにしたコマンドも ターゲットにできるので― 作業は楽しいはず さらなる情報は参考資料にて ご確認を
Appとシーンについては たくさんの情報があります トークを用意しているので 新たなAPIへの理解に役立ててください 今回“App essentials in SwiftUI”では― ビューやシーン Appが どう連携するかを説明します “Document-Based Apps in SwiftUI”では― Appでのドキュメントの 開き方や管理方法を掘り下げます
新規Appを構築するため Xcodeの“new project”もアップデートしました これはマルチプラットフォーム対応の 専用テンプレートを追加して実現しました
マルチプラットフォームコード用に最適化された 新規テンプレートは― 共有コード向けに自動でグループを設定します プラットフォームに特化した コンポーネントやアセットも同様です
私たちが広げたい プロジェクト体験の1つに― AppのLaunch Screenの構成があります 今年 新しく登場したLaunch Screenの Info.plisキーです 標準的なLaunch Screenのコンポーネントを さまざまな組み合わせで宣言できるキーです 例えばデフォルトの画像や背景の色 トップバーやボトムバーを 空にするかなどの宣言です
Launch Screen用に storyboardを使っている場合は― 今後もうまく使えるものなので 切り替えの必要はありません ただし新規SwiftUIプロジェクトで storyboardを使用しないのであれば― Launch Screen設定は シンプルな代替手段になります ではiOSとiPadOS macOSで使える 新たなウィジェットについて話しましょう ウィジェットはSwiftUIでのみビルドされます Appやビューのビルドと同じく― 新規ウィジェットのプロトコルに対しては カスタム構造体を使用します
ウィジェットの型は多種多様です こちらは お勧めの音楽アルバムを 定期的に教えてくれる仕組みです ウィジェットは SiriのIntentなど 他種のデータでもビルドできます
ウィジェットのビルド方法については 多くの説明が必要ですが― スタートしやすいよう 他のトークも用意しています お勧めは “Build SwiftUI Views for Widgets”です
ではSwiftUIを使用し― Apple Watchのカスタムコンプリケーションを ビルドしてみましょう 私が作った週次コーヒー表のように フルカラーにもビルドできます ウォッチフェイス内の外観は クールな青色にカスタマイズしました
詳細は “Build Complications in SwiftUI”でご確認を コンプリケーション作成の経験がなければ― “Creating Complications on Apple Watch” から始めるといいでしょう 次にリストとコレクションの 表示の改善について話しましょう
多くのAppで リストは重要な要素です 大抵 ユーザーが利用する 主要なインターフェイスを表します 今回のリリースで複数の優れた新機能が リストに追加されました 特記すべきはアウトラインの新しいサポートです 大胆なデータ駆動型コンテンツを 簡潔に表すレギュラーリスト 子のキーパスをイニシャライザに 提供することで― コンテンツの再帰的な アウトラインの構築を可能にしました これはmacOSのシステム標準スタイルで デフォルトで表示されます iOSとiPadOSでも同様です コンテンツ重視のAppで このアウトラインが― 分裂的なプッシュ&ポップ・ナビゲーション パターンに代わることを願います リストとアウトラインの他に― スクロール可能なレイアウトでの 複数のコンテンツ表示も一般的ですね 例えばグリッドです 今年 SwiftUIはグリッドレイアウトの 遅延読み込みにサポートを加えます スクロールビューで構成され― コンテンツが滑らかにスクロールします
グリッドはさまざまな構成をサポートする 強力なレイアウトです 使用可能な空間に合わせて列数を調整し― ご覧のように横向き 縦向きに適応します
列の数と各列のサイズパラメータを 指定することもできます このように どちらの向きでも4列にそろいます SwiftUIはグリッドの 水平スクロールもサポートします 既存の垂直および水平スタックレイアウトの 遅延読み込みバージョンも公開しており― スクロール可能なレイアウトを カスタムし構築できます 例えば この不均整な画像ギャラリー 詳しく見てみましょう
すべてのギャラリーコンテンツに 遅延垂直スタックを使用しています
switch文には新しいビュービルダー サポートを使用し― スタック内の異なるレイアウトの 画像切り替えを簡単にしました 例えば上部に大きな画像を1つ表示 不均整な3つの画像をグループにし― さらに小さい画像を短い行に 遅延読み込み垂直スクロールスタックを 組み合わせて作った― シームレスなギャラリーです リストとコレクションは SwiftUIの強力な機能です ここでは一部の機能のみを取り上げました “Stacks, Grids, and Outlines in SwiftUI” で詳細をご覧ください 次はツールバーとコントロールについて テイラーが説明します ありがとう マット SwiftUIの新しいDocument Groupや コレクションビューを使えば― Appのモデルに 簡単に命を吹き込めます 次はSwiftUIの強力なツールバーサポートと コントロールをカスタマイズする方法です 全プラットフォームのツールバーと Appにアップデートがあります macOS Big Surの美しいニュールック 一新されたiPadシステム体験 watchOSの主要なアクション これらのために SwiftUIはAPIを新しくしました 使用したのは新しいツールバーモディファイアです ツールバーItemsはSwiftUIの 他の部分と同じビューで構成されています ここではボタンです デフォルトで慣用的な場所に配置されますが ツールバーItemsでカスタマイズできます 初期のアクションでは デフォルト配置ですが― 他の配置も可能です 例えば確認とキャンセルのモーダルアクション セマンティックプレースメントの例で― ツールバーItemの役割をSwiftUIが理解し 自動的に適切な場所を見つけます 別の例はAppでアイテムを 目立たせるためのプリンシパル配置です ご覧のように iPadや―
macOSにて ツールバーItemsはポジショナル配置で― アイテムの配置を さらにデザイン管理することが可能です 特に狭いサイズでは 一般的にアイテムは下部のツールバーにあります bottomBar配置では それを明確に指定できます
今の例でSwiftUIの新しいラベルビューに 気づいたかもしれませんね 詳しく見てみましょう これはタイトルとアイコンを組み合わせた表現で UI要素のラベル付けに使用できます タイトルのローカリゼーションキーとして 使用される文字列と― システムイメージ名またはSF Symbolがあります シンボルがmacOSで利用可能なだけでなく― Appで使える 新しいものが何百種もあります SF Symbols 2.0の新たな 機能強化についての話は尽きません このラベルの構成は 完全な形のほうが便利で― タイトルとアイコンに 任意のビューを使います その力はセマンティックスから タイトルとアイコンに提供され― 使用場所に基づいて適切に処理されます ツールバーの例に戻りましょう ツールバーのコンテキストでは― アイコンがボタンのラベルとして デフォルトで視覚的に示されます アクセシビリティの目的でタイトルも出ます ツールバーとコンテキストメニュー リストに共通する動作です このリストには 複数行のラベルが含まれており― タイトルは画像サイズに関係なく 完璧にそろいます 異なるダイナミックタイプサイズを使うと ラベルの力が特に発揮されます これはレイアウトを デフォルトの大サイズで表示しています 超特大に変えるとアイコンとタイトルの 両方が自動的に更新され― テキストは うまくリフローし リストの行が増えます アクセシビリティのサイズが大きくなると さらに専門化されます ラベルはテキストの表示量を最大化するため アイコンの下で折り返すよう更新されました ラベル要素のアイコンだけを持つ ツールバーのような簡潔なコンテキストは― 追加のヘルプやコンテキストの提供が さらに重要です 新しいヘルプモディファイアでは コントロールが持つ効果の説明を添付でき― それはmacOSのツールチップとして現れます
すばらしいのは このモディファイアが 全プラットフォームで利用できることです アクセシビリティのヒントも提供し― Appにより良い ボイスオーバー体験を提供します iPhoneでも同じツールバーItemsで 同様の体験ができます プログレス ボタン 新しい進行状況エントリを記録
宣言型のSwiftUIは全ての人々の App体験を改善します
人々とコントロールの触れ合いに さらなる柔軟性とパワーをもたらすのが― キーボードショートカットモディファイアです これらはアクセスを容易にする目的で シーンコマンドによく使われます iPadのキーボードショートカットや― マットが示したとおり macOSのメインメニューにもあります キーボードショートカットは画面上の 他のコントロールにも使用できます 例えばキャンセルやデフォルトの アクションボタン エスケープとリターンキーの キーボードショートカットがあります キーボードからTVリモコン watchOSデジタルクラウンまで― フォーカスは入力内容を App上でルーティングします デフォルトのフォーカスサポートで― フォーカスの開始点とAppの デフォルトからの変化を制御できます “Build SwiftUI Apps For tvOS”で このサポートの詳細を説明します SwiftUIを使ってtvOS Appを 構築するヒントも話します
App全体で使用できる 新しいコントロールも複数あります まずプログレスビュー 時間経過と共に確定と不確定の 進行状況を表示するのに使います 線形と円形のプログレスビューがあり― 円形は 人気の回転スタイルで 不確定の進行状況を表します
似ている新コントロールはゲージです ゲージは全体的な容量と比較し 値のレベルを表すのに使います これは円形のwatchOSゲージで記録した 私の庭の土壌の酸性度です ゲージは追加オプションでカスタマイズできます トマトはデリケートなので 正確なpH値を把握したいところです そこで現在の値のラベルを 追加表示しました 最小値と最大値のラベルが追加可能です 場合によっては画像アイコンも使えますが ここではpH値をテキスト表示します
このコードスニペットはSwiftの 複数末尾クロージャ構文も際立たせます より複雑になったゲージの表現を 自然に進化させたものです 全プラットフォームでツールバーを作る 表現力豊かな方法であり― ツールバーの内外でコントロールの動作を 微調整する手段でもあります 次にSwiftUIを楽しく活用するための 新しい方法を見てみましょう macOSのBig Surで刷新されたのが― メニューバーの通知センターと 新しいコントロールセンターです 共にSwiftUIで構築されています 各種モジュールの内外で 滑らかなアニメーションを実現するため― Appで使用可能な SwiftUIの新機能を使います 私のお気に入りのアルバムを集めた UIの例です アルバムのスクロールグリッドと 選択したアルバムの列から成ります 選択時 アルバムが突然入るのでなく グリッドから流れるように移したいものです マッチド・ジオメトリ・エフェクトを 使えば簡単です このモディファイアはグリッドと 選択したアルバムの双方に適用できます 2つのビューをつなぐには アルバムのアイデンティファイアを用いましょう アイデンティファイアが関連している 名前空間も使います この場合はビューに対応する名前空間です
これだけで目的のエフェクトが実現します アルバムが別のセクションへ移る際― SwiftUIが自動的にフレーム補間し シームレスに遷移します
他にもContainerRelativeShapeという 新しいツールがあります 隣接するシェイプと類似した パスをたどるという新しい機能です お気に入りアルバムのウィジェットで 見てみましょう アルバムのアートワークのクリップは ウィジェットと同様に角が丸くなっています ぴったり はまりますね パディングを変えると良さが分かります オフセットをコンテナのシェイプに 合わせるので― ContainerRelativeShapeを使い クリッピングが成功します
オフセットを考慮し 自動的に同心性を保つのです
テキスト関連の体験も向上しました カスタムフォントはDynamic Typeの変更にも 自動で対応します テキスト内にイメージを埋め込めるので― イメージがテキストと一体化して動き Dynamic Typeに反応します レイアウトなどテキスト以外のメトリクスの カスタムには新しいProperty Wrapperです Dynamic Typeのサイズに合わせ 基準値を自動的に変えます アクセシビリティのサイズが大きくなっても 反応性の高いカスタムレイアウトが作れるのです 詳細は別のトークで話します Appを輝かせる 新しいフォントや文字入力機能も紹介します クリエイティブなカスタムビューを 構築する新しいツールもあります スタイリングの改良点は まだあります システムコントロール画面さえ 同じスタイルに統一できます カスタムアクセントカラーを用いると Appが際立ちます 今年はmacOSでの アクセントカラーのカスタマイズが可能になり Xcode12のアセットカタログで カスタマイズのサポートも新しくなりました すべてのプラットフォームで 同じ色合いを指定できます App全体にテーマカラーを 適用する場合もあれば― 特定のコントロールの色を カスタマイズしたい時もあるでしょう
iPadOSとmacOSのサイバーアイコンは Appの色に連動しますが― listItemTintを使うとカスタマイズ可能です アイテム別やセクション全体で 色を変えられます macOSのサイドバーも同じです システムのアクセントカラーの変更に モディファイアが適切に反応します
watchOSでも同じモディファイアで 標準文字盤の背景色を変えられます 同様に他のコントロールにも適用可能です
新たなスタイルカスタム機能で ボタンやトグルの色付けが可能になりました これはデフォルトの緑色から 全体をアクセントカラーに変更しました 新しいビューとインタラクションで Appがグレードアップします 最後に忘れてはならないのが― システムの機能やサービスの長所を Appに統合する新しい方法です
URLを開く高機能APIが 全プラットフォームで利用可能になりました 例えば新しいLink ViewでURLを開き リンクをラベル付けします ラベルが付いた視覚要素を作成し デフォルトのブラウザでURLを開きます
さらにユニバーサルリンクから 他のAppへ入ることもできます これはニュースの例
リンクはウィジェット内でも有効です App内に戻るリンクを 貼ることも可能です URLをプログラムで開くことが 必要な場合もあります その環境でopenURLのアクションを取ると― オプションのコンプリーションハンドラで URLを開けます
特定のビューなので自動的に SwiftUIがウィンドウ上にURLを開きます iPadOS13の更新でSwiftUIは ドラッグ&ドロップによる― 他のAppとのやり取りが 可能になりました Appの強化と統合が 実現したのです
iOS14とmacOSのBig Surでは APIが新しいフレームワークに構築され― ドラッグされるコンテンツのアイデンティフィアが 使うのはUniform Type Identifiersです SwiftUI全体に適用されるので App全体で利用できます カスタムエクスポート・インポートや イントロスペクションに使えます 記述を読みやすくしたり 適合性を検証することも可能です “Document-Based Apps in SwiftUI”で 詳しく説明します インポートとエクスポートの違いなどです AppleのWebサイトもご覧ください
他のサービスへの統合例の最後は “Appleでサインイン”ボタンです これも高性能のSwiftUI APIで すべてのプラットフォームで利用可能です AuthenticationServicesとSwiftUIを インポートするだけで新しいAPIが得られます 新たなフレームワークは不要です SwiftUIのビューやモディファイアを 提供するものは他にもあります ビデオプレーヤー マップ App Clipオーバーレイ等 最新機能を簡単に取り込めます
watchOSも含め 完全にマルチプラットフォームなので― 1つのプラットフォームで学んだ方法を 他に応用できます 新しいSwiftUIのシステム機能に― Appを統合し活用する方法を いくつかご紹介しました 多くの新機能やAPIがあるので― 冒頭でも述べたとおり すべてを網羅することはできません 最後にひと言 いくつかの例で見たように言語自体の改良により SwiftUIのコードが改善しました 詳しくは“What's New in Swift”を ご覧ください switchやif-letのサポートなど シンタックスの改良例を紹介します コンパイラの診断性能も上がり ビルドのエラーを迅速に指摘します パフォーマンスも向上しました コードサイズは縮小し コードの完成までの時間は短縮しました 改良によりSwiftUIの使用が より楽しめるでしょう 今年も新機能を共有でき うれしく思います 最後になりましたが 謝意を 皆さん ご協力ありがとうございます Feedback Assistantのレポートや SNSコメントも感謝します チュートリアルに時間を費やし プロトタイプなどを構築してくれた方々 皆さんの熱意には頭が下がります 今後も共に進化していきましょう
-
-
1:26 - Hello World
@main struct HelloWorld: App { var body: some Scene { WindowGroup { Text("Hello, world!").padding() } } }
-
1:56 - Book Club app
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
4:46 - Settings
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() @SceneBuilder var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) Settings { BookClubSettingsView() } #endif } } struct BookClubSettingsView: View { var body: some View { Text("Add your settings UI here.") .padding() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
5:10 - Document groups
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } } } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
5:49 - Custom Commands
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } .commands { CommandMenu("Shapes") { Button("Add Shape...", action: addShape) .keyboardShortcut("N") Button("Add Text", action: addText) .keyboardShortcut("T") } } } func addShape() {} func addText() {} } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
7:55 - Widgets
import SwiftUI import WidgetKit @main struct RecommendedAlbum: Widget { var body: some WidgetConfiguration { StaticConfiguration( kind: "RecommendedAlbum", provider: Provider(), placeholder: PlaceholderView() ) { entry in AlbumWidgetView(album: entry.album) } .configurationDisplayName("Recommended Album") .description("Your recommendation for the day.") } } struct AlbumWidgetView: View { var album: Album var body: some View { Text(album.title) } } struct PlaceholderView: View { var body: some View { Text("Placeholder View") } } struct Album { var title: String } struct Provider: TimelineProvider { struct Entry: TimelineEntry { var album: Album var date: Date } public func snapshot(with context: Context, completion: @escaping (Entry) -> ()) { let entry = Entry(album: Album(title: "Untitled"), date: Date()) completion(entry) } public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [Entry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = Entry(album: Album(title: "Untitled #\(hourOffset)"), date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
-
8:31 - Complications using SwiftUI
struct CoffeeHistoryChart: View { var body: some View { VStack { ComplicationHistoryLabel { Text("Weekly Coffee") .complicationForeground() } HistoryChart() } .complicationChartFont() } } struct ComplicationHistoryLabel: View { ... } struct HistoryChart: View { ... } extension View { func complicationChartFont() -> some View { ... } }
-
9:22 - Outlines
struct OutlineContentView: View { var graphics: [Graphic] var body: some View { List(graphics, children: \.children) { graphic in GraphicRow(graphic) } .listStyle(SidebarListStyle()) } } struct Graphic: Identifiable { var id: String var name: String var icon: Image var children: [Graphic]? } struct GraphicRow: View { var graphic: Graphic init(_ graphic: Graphic) { self.graphic = graphic } var body: some View { Label { Text(graphic.name) } icon: { graphic.icon } } }
-
10:09 - Adaptive grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:28 - Fixed-column grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: Array(repeating: GridItem(), count: 4)]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:38 - Horizontal grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView(.horizontal) { LazyHGrid(rows: [GridItem(.adaptive(minimum: 110))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:58 - Lazy stacks
struct WildlifeList: View { var rows: [ImageRow] var body: some View { ScrollView { LazyVStack(spacing: 2) { ForEach(rows) { row in switch row.content { case let .singleImage(image): SingleImageLayout(image: image) case let .imageGroup(images): ImageGroupLayout(images: images) case let .imageRow(images): ImageRowLayout(images: images) } } } } } }
-
12:24 - Toolbar modifier
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } private func recordProgress() {} }
-
12:40 - ToolbarItem
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } } private func recordProgress() {} }
-
12:47 - Confirmation and cancellation toolbar placements
struct ContentView: View { var body: some View { Form { Slider(value: .constant(0.39)) } .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Save", action: saveProgress) } ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: dismissSheet) } } } private func saveProgress() {} private func dismissSheet() {} }
-
13:00 - Principal toolbar placement
struct ContentView: View { enum ViewMode { case details case notes } @State private var viewMode: ViewMode = .details var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem(placement: .principal) { Picker("View", selection: $viewMode) { Text("Details").tag(ViewMode.details) Text("Notes").tag(ViewMode.notes) } } } } }
-
13:17 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
13:38 - Label
Label("Progress", systemImage: "book.circle")
-
14:06 - Label expanded form
Label { Text("Progress") } icon: { Image(systemName: "book.circle") }
-
14:24 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
14:36 - Context menu Labels
struct ContentView: View { var body: some View { List { Text("Book List Row") .contextMenu { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } Button(action: addToFavorites) { Label("Add to Favorites", systemImage: "heart") } Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func addToFavorites() {} private func shareBook() {} }
-
14:39 - List Labels
struct ContentView: View { var body: some View { List { Group { Label("Introducing SwiftUI", systemImage: "hand.wave") Label("SwiftUI Essentials", systemImage: "studentdesk") Label("Data Essentials in SwiftUI", systemImage: "flowchart") Label("App Essentials in SwiftUI", systemImage: "macwindow.on.rectangle") } Group { Label("Build Document-based apps in SwiftUI", systemImage: "doc") Label("Stacks, Grids, and Outlines", systemImage: "list.bullet.rectangle") Label("Building Custom Views in SwiftUI", systemImage: "sparkles") Label("Build SwiftUI Apps for tvOS", systemImage: "tv") Label("Build SwiftUI Views for Widgets", systemImage: "square.grid.2x2.fill") Label("Create Complications for Apple Watch", systemImage: "gauge") Label("SwiftUI on All Devices", systemImage: "laptopcomputer.and.iphone") Label("Integrating SwiftUI", systemImage: "rectangle.connected.to.line.below") } } } }
-
15:28 - Help modifier
struct ContentView: View { var body: some View { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } .help("Record new progress entry") } private func recordProgress() {} }
-
16:12 - Keyboard shortcut modifier
@main struct BookClubApp: App { var body: some Scene { WindowGroup { List { Text("Reading List Viewer") } } .commands { Button("Previous Book", action: selectPrevious) .keyboardShortcut("[") Button("Next Book", action: selectNext) .keyboardShortcut("]") } } private func selectPreviousBook() {} private func selectNextBook() {} }
-
16:28 - Cancel and default action keyboard shortcuts
struct ContentView: View { var body: some View { HStack { Button("Cancel", action: dismissSheet) .keyboardShortcut(.cancelAction) Button("Save", action: saveProgress) .keyboardShortcut(.defaultAction) } } private func dismissSheet() {} private func saveProgress() {} }
-
17:08 - ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) } }
-
17:19 - Circular ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) .progressViewStyle(CircularProgressViewStyle()) } }
-
17:25 - Activity indicator ProgressView
struct ContentView: View { var body: some View { ProgressView() } }
-
17:32 - Gauge
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } } }
-
17:52 - Gauge with current value label
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } } }
-
18:00 - Gauge with minimum and maximum value labels
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } minimumValueLabel: { Text("3") } maximumValueLabel: { Text("10") } } }
-
18:57 - Initial Album Picker
struct ContentView: View { @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:17 - Matched geometry effect Album Picker
struct ContentView: View { @Namespace private var namespace @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } .matchedGeometryEffect(id: album.id, in: namespace) } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) .matchedGeometryEffect(id: album.id, in: namespace) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:53 - Container Relative Shape
struct AlbumWidgetView: View { var album: Album var body: some View { album.image .clipShape(ContainerRelativeShape()) .padding() } } struct Album { var name: String var artist: String var image: Image }
-
20:34 - Dynamic Type scaling
struct ContentView: View { var album: Album @ScaledMetric private var padding: CGFloat = 10 var body: some View { VStack { Text(album.name) .font(.custom("AvenirNext-Bold", size: 30)) Text("\(Image(systemName: "music.mic")) \(album.artist)") .font(.custom("AvenirNext-Bold", size: 17)) } .padding(padding) .background(RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color.purple)) } } struct Album { var name: String var artist: String var image: Image }
-
22:08 - Initial Sidebar List
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") Label("Rewards", systemImage: "seal") Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } } .listStyle(SidebarListStyle()) } } }
-
22:17 - List Item Tint in Sidebars
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } .listStyle(SidebarListStyle()) } } }
-
22:33 - List Item Tint on watchOS
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } } } }
-
22:46 - SwitchToggleStyle tint
struct ContentView: View { @State var order = Order() var body: some View { Toggle("Send notification when ready", isOn: $order.notifyWhenReady) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } struct Order { var notifyWhenReady = true }
-
23:15 - Link
let appleURL = URL(string: "https://developer.apple.com/tutorials/swiftui/")! let wwdcAnnouncementURL = URL(string: "https://apple.news/AjriX1CWUT-OfjXu_R4QsnA")! struct ContentView: View { var body: some View { Form { Section { Link(destination: apple) { Label("SwiftUI Tutorials", systemImage: "swift") } Link(destination: wwdcAnnouncementURL) { Label("WWDC 2020 Announcement", systemImage: "chevron.left.slash.chevron.right") } } } } }
-
23:56 - OpenURL Environment Action
let customPublisher = NotificationCenter.default.publisher(for: .init("CustomURLRequestNotification")) let apple = URL(string: "https://developer.apple.com/tutorials/swiftui/")! struct ContentView: View { @Environment(\.openURL) private var openURL var body: some View { Text("OpenURL Environment Action") .onReceive(customPublisher) { output in if output.userInfo!["shouldOpenURL"] as! Bool { openURL(apple) } } } }
-
24:44 - Uniform Type Identifiers
import UniformTypeIdentifiers extension UTType { static let myFileFormat = UTType(exportedAs: "com.example.myfileformat") } func introspecContentType(_ fileURL: URL) throws { // Get this file's content type. let resourceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey]) if let type = resourceValues.contentType { // Get the human presentable description of the type. let description = type.localizedDescription if type.conforms(to: .myFileFormat) { // The file is our app’s format. } else if type.conforms(to: .image) { // The file is an image. } } }
-
25:16 - Sign in with Apple Button
import AuthenticationServices import SwiftUI struct ContentView: View { var body: some View { SignInWithAppleButton( .signUp, onRequest: handleRequest, onCompletion: handleCompletion ) .signInWithAppleButtonStyle(.black) } private func handleRequest(request: ASAuthorizationAppleIDRequest) {} private func handleCompletion(result: Result<ASAuthorization, Error>) {} }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。