-
SwiftUIの基本
Appleの宣言型ユーザーインターフェイスフレームワークである、SwiftUIのツアーにご参加ください。ビュー、状態変数、レイアウトなど、SwiftUIでアプリを構築するうえで基本となる概念について解説します。豊富な機能により充実した体験を提供するアプリを実現し、独自性のあるカスタムコンポーネントを作成するうえで役立つ、多彩なAPIもご紹介します。SwiftUI初心者の方も、経験豊富なデベロッパの方も、SwiftUIのメリットを活用して優れたアプリを構築する方法を習得できます。
関連する章
- 0:00 - Introduction
- 1:34 - Fundamentals of views
- 13:06 - Bulit-in capability
- 17:36 - Across all platforms
- 20:30 - SDK interoperability
リソース
関連ビデオ
WWDC24
WWDC20
-
ダウンロード
こんにちは Taylorです 「SwiftUI Essentials」へようこそ SwiftUIはAppleの宣言型ユーザー インターフェイスフレームワークです Appleのすべてのプラットフォームで アプリの構築に使用されます 新規アプリの基盤として Apple内で広く採用されており 既存のアプリについても 段階的に採用されています 新しいアプリや新機能を開発する時は SwiftUIは最適なツールです これにはいくつか理由があります SwiftUIには幅広い機能が備わっています これにより デバイスを活用し Appleのプラットフォームで自然に利用でき 優れたインタラクティブ機能が追加された アプリを開発できます さらにこれらの機能を追加するための コードは少なくてすみます プロトタイプから本番環境への 迅速な移行が可能なので 独自のアプリの開発に集中できます SwiftUIは 段階的な導入に対応しているため 必要な場所でのみ利用できます アプリ全体で SwiftUIを利用する必要はありません したがって誰でも簡単に SwiftUIを使用したアプリの 構築方法を学習できます SwiftUIがこれらの特徴を どのように実現しているかを理解して 最大限に活用しましょう まず ビューの基本的な仕組みについて 説明します その後 SwiftUIに組み込まれた いくつかの機能を紹介し Appleのプラットフォームでの 役割について見ていきます 最後に 他のフレームワークと統合する SwiftUIの機能について説明します
その前に SwiftUIとその利用について もう1つ紹介したいことがあります 飼っているペットの中でどの種類が一番か 議論になることがよくあります これをできるだけ客観的な方法で 解決したいと思いました どのペットが 最も上手に芸をできるかです 厳しいコンテストもあります
このセッションでは SwiftUIを使って ペットとその芸を追跡し それを比較するアプリを作成します 何から始めればよいでしょう SwiftUIではビューから始まります ビューはユーザーインターフェイスの 基本的な構成要素であり SwiftUIでのあらゆる操作で重要です 画面に表示されるすべてのピクセルは 何らかの形でビューによって定義されます SwiftUIビューを特別なものにしている 特徴は3つあります 宣言型 コンポジション性 状態駆動型です ビューは宣言によって表現されます
ユーザーインターフェイスに 表示したいビューを記述すると SwiftUIがその結果を生成します テキスト SF Symbolsを使った画像 ボタンなどのコントロールを生成できます
このコードで作成される横方向スタックは アイコンとタイトルの 組み合わせによるラベル スペーサ テキストで構成されます この宣言型構文は 他のコンテナにも適用されます 例えばスクロールリストです
このリストにはペットのコレクションが 追加され それぞれに横方向のスタックが作成されます すべてのペットが 「Whiskers」というわけではないので スタックのビューを更新して 代わりに 各ペットのプロパティを使用します
このUIを作成するために必要なアクションを 記述する必要はありませんでした 例えば リストの行を追加したり 削除したりするアクションです
これが宣言型プログラミングと 命令型プログラミングの違いです ペットに芸を教えたことがある方は 命令型コマンドになじみがあるでしょう
命令型プログラミングと同様に ここでRufusに ホームランを打つ手順を 1つずつ指示できます Rufus 来なさい Rufus バットを持ちなさい Rufus 打席について などなど 各手順を記述します
これに対し 宣言型のペットの芸では 起こってほしいことを記述し あらかじめ準備した犬にやらせます あとは必要なことを言うだけです 「Rufusにホームランを打ってほしい」 芸の一部をカスタマイズすることもできます 例えば Rufusが着るオーダーメイドのシャツを 用意したりなどです 宣言型と命令型のプログラミングは 相互排他的ではありません 宣言型コードでは そこに至る手順ではなく 期待される結果に集中することができます 命令型コードは 状態を変更する時や 既存の宣言型コンポーネントがない場合に 適しています SwiftUIはこれら両方に対応します この好例はボタンです ボタンは宣言によって ユーザーインターフェイスに追加され その宣言には タップされた時に実行する アクションが含まれます このアクションは命令型コードを使用して 変更を行います この例では 新しいペットをリストに追加します
SwiftUIビューはUIの現在の状態が どうであるべきかという記述です 時間の経過とともに命令を受け取る 長期間存在するオブジェクトインスタンス ではありません これがSwiftUIビューが 値型である理由であり クラスではなく構造体で定義されます
SwiftUIはこれらの記述を受け取り 効率的なデータ構造を作成して それらを表現します このデータ構造は バックグラウンドで維持されます そして様々な出力の作成に使用されます 例えば 画面に表示される要素や ジェスチャおよび ビューのインタラクティブな側面 そのアクセシビリティの表現などです ビューは宣言型記述にすぎないため 1つのビューを複数に分割しても アプリのパフォーマンスには影響しません 最適なパフォーマンスを得るために コードの整理方法を 犠牲にする必要はありません
コンポジションは SwiftUI全体で使用され すべてのユーザーインターフェイスの 重要な要素です 先ほど作成した HStackはコンテナビューであり レイアウトを目的としています 横方向スタックに子を配置しています コンテナビューを使った調整や実験は SwiftUIではとても簡単です コード自体は 作成するビューの階層と似ています
横方向スタックには 次の3つのビューがあります 画像 縦方向スタック スペーサです 縦方向スタック自体にも 2つのビューがあります ラベルとテキストです
この構文はViewBuilderクロージャを使って コンテナの子を宣言します この例では HStackの イニシャライザを使っています これには ViewBuilder content パラメータがあります これはSwiftUIのすべてのコンテナビューで 使用されるパターンの1つです
コンポジションはビューモディファイアと 呼ばれる別のSwiftUIパターンで 重要な役割を果たします ビューモディファイアは ベースビューに修飾を適用し そのビューのあらゆる側面を変更できます Whiskerのかわいい写真から 始めましょう まず円形にクリップし 影を加え 上に緑色の境界線を重ねます 彼女の好きな色です
構文的にはコンテナビューと 大きく異なるように見えますが 結果としては同じような 階層型構造になります 階層と効果の順序は モディファイアの順序に基づいて 定義されています 複数のモディファイアをつなぐことで 結果を生成する手順と そのカスタマイズ方法が明確になります すべて読みやすい構文で 構成されています
ビューの階層はカスタムビューと ビューモディファイアにカプセル化できます カスタムビューはViewプロトコルに準拠し 表現するビューを返す bodyプロパティがあります ボディから返されたビューは 先ほどと同じ ビュー構築構文を使用することで 同じコンポジション機能と 迅速な反復処理を可能にします 追加のビューのプロパティを作成して 希望に合わせてコードを 整理することができます プロファイル画像の構築を リファクタリングして プライベートビュープロパティにしました
このように段階的に進めていくことで 行のビューを 希望通りに繰り返し作成できます
カスタムビューにはボディの作成方法を 変更する入力を設定できます この行に示される ペットのプロパティを追加して そのプロパティをボディから返される ビューで使用しました
この変更により 同じビューを再利用して Whiskersに関する情報を 表示できます RoofusとBubblesも表示できます
カスタムビューは 他のビューと同様に使用できます ここではペットごとに作成するビューとして リストで使用しました
リストはビューコンポジションの力を よく表しています このリストイニシャライザには コレクション引数があります ForEachビューを作成するのに便利です ForEachはコレクションに含まれる 要素ごとにビューを生成し それらをコンテナに提供します このビューベースの リストイニシャライザにより より高度な構造を作成できます 例えば セクションごとにまとめられた 複数のデータコレクションです 1つは自分のペット用 もう1つは他のユーザーのペット用です
リストはビューモディファイアを使用して カスタマイズすることもできます 例えば各行に スワイプアクションを追加します
追加のコンテナとモディファイアの コンポジションによって アプリ全体を少しずつ構築できます
SwiftUIのビューの3つ目の特性は 状態駆動型であることです 時間の経過とともに ビューの状態が変化した時 SwiftUIによってUIが自動的に 最新の状態に保たれ 定型文とアップデートの 両方のバグを排除できます SwiftUIは内部で実行されている ユーザーインターフェイスの 表現を維持します データが変化すると 新しいビュー値が作成され SwiftUIに渡されます SwiftUIはこれらの値を使用して 出力の更新方法を決定します これでペットとその芸のリストが アプリに追加されました しかしペットコンテストで 最も重要なのは 最高の芸を持つペットに 報酬を与えることです これはSheldonです 報酬としてイチゴを欲しがっています 各行にスワイプアクションを追加しました 準備は整っています
ボタンをタップすると アクションが呼び出されます 関連するペットオブジェクトが変更され hasAwardがtrueに変更されます
SwiftUIはこのペットに依存する ビューを追跡します 例えば行ビューです
ペットへの参照が含まれており そのボディから ペットが報酬を受けたかどうかを読み取り 依存関係を確立します
SwiftUIは更新されたペットで このビューのボディを再度呼び出します
画像を含む結果が返されるようになり Sheldonの報酬を反映します
SwiftUIはこの結果に基づいて 出力を更新し 新しい画像を画面に表示します
ビューがボディで使用するデータはすべて そのビューの依存関係です このアプリでは Observablepetクラスを作成しました SwiftUIはビューボディで使用される 特定のプロパティへの 依存関係を作成します SwiftUIには状態を管理する 複数のツールがあります その他にStateとBindingの 2つが重要です Stateはビューに対して データの新しい内部ソースを作成します ビューのプロパティを @Stateとしてマークすると SwiftUIはそのストレージを管理し それを返して ビューが読み書きできるようにします
Bindingは他のビューの状態に対する 双方向の参照を作成します
これらを使用する別のビューも 作成しました このビューでは ペットの芸を評価できます Stateを使って 現在の評価を追跡し 時間の経過とともに変更します 値は中央に目立つように表示され 値を増減するボタンが2つ付いています
先ほどの例と同様 ボタンがタップされると そのアクションが呼び出されます 今回は ビューの内部のStateを増やします
SwiftUIはこの変化に気づき RatingViewのボディを呼び出します 新しいテキスト値が返され 画面上の結果が更新されます
今のところすぐに変更が行われています アニメーションはありません SwiftUIのアニメーションは 既に説明したのと同じ データ駆動型更新に基づいて 構築されます
withAnimationで この状態変更をラップすると その結果のビュー更新はデフォルトの アニメーションと共に適用されます
SwiftUIはデフォルトの クロスフェードトランジションを テキストに適用しました トランジションを カスタマイズすることもできます
この場合 数値テキストのコンテンツトランジションが 適しています
状態とアニメーションで 必要なやり取りを行うカプセル化された ビューコンポーネントを作りました 最終的には アプリの他の部分についても このビューを作成します
もう1つのビューは RatingContainerViewです これはボディでGaugeと RatingViewを組み合わせます
現在 これらのビューには それぞれ独自の状態があり 独立した独自の正確なソースとして 役割を果たします しかし これは評価ビューが 自身の状態を増加させた時に コンテナビューの状態とゲージが 変化しないということです
RatingViewを更新して 入力としてBindingを受け取り 双方向の参照がコンテナビューによって 提供できるようにします
コンテナビューの状態が 信頼できる唯一のソースとなり Gaugeに値を提供し Bindingを RatingViewに提供します 同期して更新され アニメーション化された状態変化が ゲージにも適用されます
SwiftUIには様々なレベルの 機能が組み込まれており 有利な条件で アプリの作成を開始できます
私はペットやペットの芸を 追跡するアプリに取りかかり 今のところ満足しています SwiftUIは自動的に多くの側面で アダプティビティを提供します
このアプリはダークモードでも 問題なく表示されます Dynamic Typeなどの アクセシビリティ機能も サポートされています ローカライズする準備もできています 例えば 右から左に書く 疑似言語でプレビューすると ヘブライ語やアラビア語で どのように表示されるかを確認できます
これはXcodeプレビューの 優れた点の1つです 様々なコンテキストでどのように見えるか 一目でわかります この処理はコードを記述する際に実行され アプリを繰り返し実行する必要はありません
プレビューはインタラクティブで デバイス上で直接行えます 作業中の機能がどんな感じか正確に 理解できます それも作成している最中にです
SwiftUIの宣言型ビューのメリットの1つは アダプティビティです SwiftUIの提供するビューでは その視覚的構造ではなく 機能の目的を記述することがよくあります
先ほどスワイプアクションがボタンなどの ビューで構成されていることを紹介しました ボタンはアダプティブビューの好例です ボタンには2つの基本的な特性があります アクションと アクションを説明するラベルです
これらは様々なコンテキストで 使用できますが ラベルの付いたアクションの目的は 常にそのままです
これらは様々なスタイルに適応します 境界なし 境界付き 突出型などです 様々なコンテキストに自動的に適応します スワイプアクション メニュー フォームなどです このパターンはSwiftUIの すべてのコントロールに当てはまります トグルも同様です トグルには独自のスタイルがあります 例えば スイッチ チェックボックス トグルボタンがあります 異なるコンテキストでは 慣用的なスタイルで表示され オン/オフを切り替えるものを表現します
SwiftUI全体のビューの多くは 同じアダプティビティがあり コンポジションを活かして 行動に影響を与え カスタマイズを可能にします これは一部のビューモディファイアにも 当てはまります 私の好きな例は検索機能です これはペットのリストに適用します
searchableモディファイアを追加すると 適用先のビューが検索可能であると 記述していることになります SwiftUIはすべての詳細を担当し 慣用的な方法で実現します その他のモディファイアを 段階的に採用することで 体験をカスタマイズすることもできます 提案 スコープ トークンの追加などです
SwiftUIの宣言ビューと アダプティブビューは わずか数行のコードに 多くの機能が詰め込まれています
ボタン トグル ピッカーなどの コントロール NavigationSplitViewや カスタマイズ可能な 複数列テーブルなどのコンテナビュー シートやインスペクタなどの プレゼンテーションをはじめ 他にも使用できる例が ドキュメントに記述されています
独自のカスタム体験を 作成する準備ができたら SwiftUIにはAPIの別の層もあり 低レベルコントロールを提供します 独自のコントロールスタイルを作成できます Canvasを使用して 高パフォーマンスの命令型描画を実現したり 完全にカスタマイズされた レイアウトを作成したり カスタムMetalシェーダを SwiftUIビューに直接適用したりできます
私のアプリでは スコアボードが この低レベルツールを使用して 独自の体験を作成するのに 最適な場所でした 古典的なフリップボードを彷彿とさせます アニメーション グラフィックスのトリック いくつかのMetalシェーダを使いました
Sheldonはこの最後の芸で 着地に失敗したので 7点を付けなければなりません 次はがんばってね
SwiftUIの機能は ビューだけではありません アプリの定義全体が ビューの遵守する同じ原則に基づいて 構築されています アプリは宣言型構造体で シーンによって定義されます WindowGroupもシーンの一種です 画面に表示するコンテンツビューで 作成されます シーンを組み合わせて 構成することもできます
macOSなどの マルチウインドウプラットフォームでは 追加のシーンによって様々な方法で アプリの機能を操作できます
このパターンはカスタムウィジェットの 作成にも拡張できます ウィジェットは ホーム画面とデスクトップに表示され 複数のビューで構成されています スコアボードビューの一部を再利用して Sheldonの最新評価を表示します SwiftUIの機能はその動作する あらゆるプラットフォームに拡張され 成果を1つのプラットフォームにまとめたり 他のプラットフォームで ネイティブアプリを構築したりできます SwiftUIは どのAppleプラットフォーム向けの アプリを構築する場合でも利用できます。
またSwiftUIは 努力の成果を何倍にもしてくれます 一度1つのプラットフォーム向けに ユーザーインターフェースを構築すれば そのUIを他のプラットフォームに持ち込む 素晴らしいスタートになります
アダプティブビューとシーンが どのAppleプラットフォームでも 特有の見た目と操作性を実現します macOSの場合 キーボードでの操作や マルチウインドウの作成を 自動的にサポートします
検索候補機能の使用方法も同じで macOSでは標準的な ドロップダウンメニューが表示され iOSではオーバーレイリストが 表示されます
低レベルAPIによって作成された カスタムビューは どのプラットフォームでも 同じ結果が得られ 必要に応じて同じビューを 再利用するのに最適です
すべてのプラットフォームを対象に スコアボードのアニメーションを 作成できました
SwiftUIではこのように コードを共有できますが 「一度記述すれば どこでも実行できる」 わけではありません 一度学べば どのコンテキストでも どのAppleプラットフォームでも 使用できるツールセットということです
SwiftUIにはプラットフォーム間で 共通の高レベルと低レベルの コンポーネントがありますが プラットフォームごとに 専用のAPIもあります
ヒューマンインターフェイス ガイドラインには コンポーネント パターン プラットフォームに関する 考慮事項が記述されています
NavigationSplitViewは 詳細を表示するソースリストという watchOSのデザインに 自動的に適応します
スコアボードなどのカスタムビューも 再利用しました しかし watchOSに特化した変更を1つ 加えたいと思います タッチやキーボードの代わりに Digital Crownを使って 評価を素早く 選択できるようにしたいのです
同じスコアボードビュー上に watchOSのモディファイアを追加しました digitalCrownRotationです
これでDigital Crownを回すと 希望のスコアを選択できます
Macでペットとペットの芸を見直すと 過去のデータを詳しく調べて ペット間で比較したくなりました macOSの柔軟なウインドウ管理モデルを 様々なシーンタイプで活用できます 使い慣れたmacOSの コントロールライブラリ 情報密度 正確な入力を最大限に活用するビューを 使用することもできます
このアプリを visionOSに移植して 他のプラットフォームからの ビューを利用して ボリュームのあるコンテンツを 追加することもできます SwiftUIはアプリがどこへ行ってもその品質を 高めるよう支援します これも段階的な性質を持っています SwiftUIアプリではプラットフォームを 複数サポートする必要がありません 準備ができた時は 有利に導入を開始できます 最後に取り上げる分野は SwiftUI自体の 組み込み機能ではありません 他のフレームワークの機能と SwiftUIの相互運用機能です SwiftUIは各プラットフォームの SDKに含まれています 他の様々なフレームワークも SDKに含まれており それぞれ独自の魅力的な 機能を備えています これらのフレームワークを すべて使うアプリはありませんが 必要なテクノロジーを提供する フレームワークを選んで使用できます
SwiftUIはこの機能すべてに 相互運用性を提供し 多くの場合 別のビューやプロパティをアプリに 追加するのと同じくらい簡単です
UIKitとAppKitは 命令型のオブジェクト指向 ユーザーインターフェイスフレームワークです SwiftUIと同様の 構成要素を提供しますが 様々なパターンを使用して ビューの作成と更新を行います 長年にわたって利用されてきた豊富な機能は SwiftUIの基盤となっています
SwiftUIの基本機能はそれらとの シームレスな相互運用性です
UIKitまたはAppKitからのビューや ビューコントローラがある場合で SwiftUIで使用したい場合は ViewRepresentableを作成できます
これは特別な SwiftUIビュープロトコルであり 命令型コードを使用して 関連する UIKitビューまたはAppKitビューを 作成および更新します
その結果SwiftUIの宣言型ビュービルダーで 使用できるビューが生成され HStackで使用するなど 他のビューと同じように使用できます 逆もまた同じです UIKitまたはAppKitビュー階層に SwiftUIビューを埋め込みたい場合 Hosting View Controllerなどの クラスを使用できます これはルートSwiftUIビューで作成され UIKitまたはAppKitビューコントローラ階層に 追加できます Apple所有のアプリは これらのツールを使用して 段階的にSwiftUIを取り入れます SwiftUIを 既存のアプリに活用する場合も まったく新しいSwiftUIアプリを作成して Kitビューを組み込む場合でも同様です これらのツールは すべてツールボックスにあり 素晴らしいアプリを作成できます アプリが完全にSwiftUIになって SwiftUIを活用しなければならない ということは想定されていません
SDKのすべてのフレームワークには 独自の機能があります SwiftDataを使うと 永続性モデルアプリに素早く追加でき SwiftUIビューを使って それらのモデルに接続して照会するための APIが含まれています
Swift Chartsは高度にカスタマイズ可能な グラフ作成フレームワークです SwiftUIをベースに 情報の美しい視覚表現を 簡単に作成できるようにします
これらのフレームワークはすべて 優れたアプリの作成に 役立てることができます SwiftUIは宣言型 コンポジション性 状態駆動型のビューを基盤として 構築されています その上にプラットフォームに慣用的な機能と 幅広いSDKとの統合を提供します これらはすべてアプリの独自性を 高めることに集中する上で役立ちます より少ないコードで作成できます 慣用的で魅力的なアプリにつながる 幅広いコンポーネントを提供しています また あらゆるステップで段階的に 導入することができます
SwiftUIを始めてみましょう Xcodeを起動して 最初のアプリを作成してみましょう 既存のアプリに SwiftUIを組み込むこともできます SwiftUIに関する その他のセッションもご覧ください 次は「Introduction to SwiftUI」を お勧めします
SwiftUIのチュートリアルに沿って 進めましょう 様々なアプリの作成方法を 紹介しています ドキュメントにはこの他にも 様々なメリットがあります
ペットコンテストについて言えば どのペットが一番かを アプリで決めるのは難しそうです 今のところの結論は どのペットも愛すべき存在です
-
-
2:30 - Declarative views
Text("Whiskers") Image(systemName: "cat.fill") Button("Give Treat") { // Give Whiskers a treat }
-
2:43 - Declarative views: layout
HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") }
-
2:56 - Declarative views: list
struct ContentView: View { private var pets = Pet.samplePets var body: some View { List(pets) { pet in HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
3:07 - Declarative views: list
struct ContentView: View { private var pets = Pet.samplePets var body: some View { List(pets) { pet in HStack { Label(pet.name, systemImage: pet.kind.systemImage) Spacer() Text(pet.trick) } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
4:24 - Declarative and imperative programming
struct ContentView: View { private var pets = Pet.samplePets var body: some View { Button("Add Pet") { pets.append(Pet("Toby", kind: .dog, trick: "WWDC Presenter")) } List(pets) { pet in HStack { Label(pet.name, systemImage: pet.kind.systemImage) Spacer() Text(pet.trick) } } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String init(_ name: String, kind: Kind, trick: String) { self.name = name self.kind = kind self.trick = trick } static let samplePets = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking"), Pet("Roofus", kind: .dog, trick: "Home runs"), Pet("Bubbles", kind: .fish, trick: "100m freestyle"), Pet("Mango", kind: .bird, trick: "Basketball dunk"), Pet("Ziggy", kind: .lizard, trick: "Parkour"), Pet("Sheldon", kind: .turtle, trick: "Kickflip"), Pet("Chirpy", kind: .bug, trick: "Canon in D") ] }
-
5:33 - Layout container
HStack { Label("Whiskers", systemImage: "cat.fill") Spacer() Text("Tightrope walking") }
-
5:41 - Container views
struct ContentView: View { var body: some View { HStack { Image(whiskers.profileImage) VStack(alignment: .leading) { Label("Whiskers", systemImage: "cat.fill") Text("Tightrope walking") } Spacer() } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
6:23 - View modifiers
struct ContentView: View { var body: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:05 - Custom views: Intro
struct PetRowView: View { var body: some View { // ... } }
-
7:14 - Custom views
struct PetRowView: View { var body: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:20 - Custom views: iteration
struct PetRowView: View { var body: some View { HStack { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle() .stroke(.green, lineWidth: 2) } Text("Whiskers") Spacer() } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:24 - Custom views: view properties
struct PetRowView: View { var body: some View { HStack { profileImage Text("Whiskers") Spacer() } } private var profileImage: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:34 - Custom views: complete row view
struct PetRowView: View { var body: some View { HStack { profileImage VStack(alignment: .leading) { Text("Whiskers") Text("Tightrope walking") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(whiskers.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(.green, lineWidth: 2) } } } let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers") struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String init(_ name: String, kind: Kind, trick: String, profileImage: String) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage } }
-
7:41 - Custom views: input properties
struct PetRowView: View { var pet: Pet var body: some View { HStack { profileImage VStack(alignment: .leading) { Text(pet.name) Text(pet.trick) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(pet.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(pet.favoriteColor, lineWidth: 2) } } } struct Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } let id = UUID() var name: String var kind: Kind var trick: String var profileImage: String var favoriteColor: Color init(_ name: String, kind: Kind, trick: String, profileImage: String, favoriteColor: Color) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage self.favoriteColor = favoriteColor } }
-
7:53 - Custom views: reuse
PetRowView(pet: model.pet(named: "Whiskers")) PetRowView(pet: model.pet(named: "Roofus")) PetRowView(pet: model.pet(named: "Bubbles"))
-
7:59 - List composition
struct ContentView: View { var model: PetStore var body: some View { List(model.allPets) { pet in PetRowView(pet: pet) } } } class PetStore { var allPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:14 - List composition: ForEach
struct ContentView: View { var model: PetStore var body: some View { List { ForEach(model.allPets) { pet in PetRowView(pet: pet) } } } } class PetStore { var allPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:27 - List composition: sections
struct ContentView: View { var model: PetStore var body: some View { List { Section("My Pets") { ForEach(model.myPets) { pet in PetRowView(pet: pet) } } Section("Other Pets") { ForEach(model.otherPets) { pet in PetRowView(pet: pet) } } } } } class PetStore { var myPets: [Pet] = [ Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), ] var otherPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] }
-
8:36 - List composition: section actions
PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Award", systemImage: "trophy") { // Give pet award } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) }
-
9:31 - View updates
struct ContentView: View { var model: PetStore var body: some View { List { Section("My Pets") { ForEach(model.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(model.otherPets) { pet in row(pet: pet) } } } } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Award", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } } struct PetRowView: View { var pet: Pet var body: some View { HStack { profileImage VStack(alignment: .leading) { HStack(alignment: .firstTextBaseline) { Text(pet.name) if pet.hasAward { Image(systemName: "trophy.fill") .foregroundStyle(.orange) } } Text(pet.trick) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } } private var profileImage: some View { Image(pet.profileImage) .clipShape(.circle) .shadow(radius: 3) .overlay { Circle().stroke(pet.favoriteColor, lineWidth: 2) } } } class PetStore { var myPets: [Pet] = [ Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue), Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown), ] var otherPets: [Pet] = [ Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green), Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange), Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green), Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple), Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange) ] } class Pet: Identifiable { enum Kind { case cat case dog case fish case bird case lizard case turtle case rabbit case bug var systemImage: String { switch self { case .cat: return "cat.fill" case .dog: return "dog.fill" case .fish: return "fish.fill" case .bird: return "bird.fill" case .lizard: return "lizard.fill" case .turtle: return "tortoise.fill" case .rabbit: return "rabbit.fill" case .bug: return "ant.fill" } } } var name: String var kind: Kind var trick: String var profileImage: String var favoriteColor: Color var hasAward: Bool = false init(_ name: String, kind: Kind, trick: String, profileImage: String, favoriteColor: Color) { self.name = name self.kind = kind self.trick = trick self.profileImage = profileImage self.favoriteColor = favoriteColor } func giveAward() { hasAward = true } } extension Pet: Transferable { static var transferRepresentation: some TransferRepresentation { ProxyRepresentation { $0.name } } }
-
10:57 - State changes
struct RatingView: View { var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { rating -= 1 } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { rating += 1 } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
11:51 - State changes: animation
struct RatingView: View { var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:05 - State changes: text content transition
struct RatingView: View { var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:22 - State changes: multiple state
struct RatingContainerView: View { private var rating: Int = 5 var body: some View { Gauge(value: Double(rating), in: 0...10) { Text("Rating") } RatingView() } } struct RatingView: View { var rating: Int = 5 var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
12:45 - State changes: state and binding
struct RatingContainerView: View { private var rating: Int = 5 var body: some View { Gauge(value: Double(rating), in: 0...10) { Text("Rating") } RatingView(rating: $rating) } } struct RatingView: View { var rating: Int var body: some View { HStack { Button("Decrease", systemImage: "minus.circle") { withAnimation { rating -= 1 } } .disabled(rating == 0) .labelStyle(.iconOnly) Text(rating, format: .number.precision(.integerLength(2))) .contentTransition(.numericText(value: Double(rating))) .font(.title.bold()) Button("Increase", systemImage: "plus.circle") { withAnimation { rating += 1 } } .disabled(rating == 10) .labelStyle(.iconOnly) } } }
-
14:16 - Adaptive buttons
Button("Reward", systemImage: "trophy") { // Give pet award } // .buttonStyle(.borderless) // .buttonStyle(.bordered) // .buttonStyle(.borderedProminent)
-
14:53 - Adaptive toggles
Toggle("Nocturnal Mode", systemImage: "moon", isOn: $pet.isNocturnal) // .toggleStyle(.switch) // .toggleStyle(.checkbox) // .toggleStyle(.button)
-
15:19 - Searchable
struct PetListView: View { var viewModel: PetStoreViewModel var body: some View { List { Section("My Pets") { ForEach(viewModel.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(viewModel.otherPets) { pet in row(pet: pet) } } } .searchable(text: $viewModel.searchText) } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Reward", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } } class PetStoreViewModel { var petStore: PetStore var searchText: String = "" init(petStore: PetStore) { self.petStore = petStore } var myPets: [Pet] { // For illustration purposes only. The filtered pets should be cached. petStore.myPets.filter { searchText.isEmpty || $0.name.contains(searchText) } } var otherPets: [Pet] { // For illustration purposes only. The filtered pets should be cached. petStore.otherPets.filter { searchText.isEmpty || $0.name.contains(searchText) } } }
-
15:20 - Searchable: customization
struct PetListView: View { var viewModel: PetStoreViewModel var body: some View { List { Section("My Pets") { ForEach(viewModel.myPets) { pet in row(pet: pet) } } Section("Other Pets") { ForEach(viewModel.otherPets) { pet in row(pet: pet) } } } .searchable(text: $viewModel.searchText, editableTokens: $viewModel.searchTokens) { $token in Label(token.kind.name, systemImage: token.kind.systemImage) } .searchScopes($viewModel.searchScope) { Text("All Pets").tag(PetStoreViewModel.SearchScope.allPets) Text("My Pets").tag(PetStoreViewModel.SearchScope.myPets) Text("Other Pets").tag(PetStoreViewModel.SearchScope.otherPets) } .searchSuggestions { PetSearchSuggestions(viewModel: viewModel) } } private func row(pet: Pet) -> some View { PetRowView(pet: pet) .swipeActions(edge: .leading) { Button("Reward", systemImage: "trophy") { pet.giveAward() } .tint(.orange) ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name))) } } }
-
16:58 - App definition
@main struct SwiftUIEssentialsApp: App { var body: some Scene { WindowGroup { ContentView() } } }
-
17:15 - App definition: multiple scenes
@main struct SwiftUIEssentialsApp: App { var body: some Scene { WindowGroup { ContentView() } WindowGroup("Training History", id: "history", for: TrainingHistory.ID.self) { $id in TrainingHistoryView(historyID: id) } WindowGroup("Pet Detail", id: "detail", for: Pet.ID.self) { $id in PetDetailView(petID: id) } } }
-
17:23 - Widgets
struct ScoreboardWidget: Widget { var body: some WidgetConfiguration { // ... } } struct ScoreboardWidgetView: View { var petTrick: PetTrick var body: some View { ScoreCard(rating: petTrick.rating) .overlay(alignment: .bottom) { Text(petTrick.pet.name) .padding() } .widgetURL(petTrick.pet.url) } }
-
19:37 - Digital Crown rotation
ScoreCardStack(rating: $rating) .focusable() #if os(watchOS) .digitalCrownRotation($rating, from: 0, through: 10) #endif
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。