ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swift UIにおけるデータの重要事項
Appにおいてデータは難解な部分ですが、SwiftUIがあれば、試作から生産に至るまで、スムーズでデータ駆動型の経験を容易にすることが可能になります。 Source of Truthを保存し、シームレスにアップデートすることが可能な@State および@Bindingの2つのパワフルなツールについてお伝えします。また、ObservableObjectを使い、あなたのビューをデータモデルに接続する方法もご紹介します。 トリッキーな挑戦、クールな新しい解決方法を専門家が直々にお話します! このセッションを有効に活用するためには、 SwiftUIに慣れていることが望ましいです。“App essentials in SwiftUI”および"Introduction to SwiftUI"をご覧ください。
リソース
関連ビデオ
WWDC22
WWDC21
WWDC20
-
ダウンロード
こんにちは WWDCへようこそ “Swift UIにおけるデータエッセンシャル” “Swift UIにおけるデータエッセンシャル” へようこそ SwiftUIチーム エンジニアのカートです 後ほど 友人であり同僚の ルカとラージも登場します 今回は主に3つのテーマを扱います まずSwiftUI Appのデータフローをお話しし StateやBindingなどにも触れます 次にこうしたアイディアをどうやってビルドし Appのデータモデルを設計するかを説明します 最後にデータモデルをAppに統合するための テクニックをご紹介します その中でSwiftUIビューのライフサイクルや 便利な新機能についてもご説明します Swiftの値や参照型がSwiftUIのデータフローと どのように相互作用するのか理解が深まります 私からはSwiftUIの 基本的なデータフローを説明し 新しいSwiftUIビューを作る際に 考慮すべき質問についてお話しします ルカはブッククラブに夢中です 彼はクラブメンバーの読書記録や メモが残せるAppが必要だと思いました 私も読書が好きなので 一緒にAppを作ることにしました
まず 読んでいる本を表示する ビューのプロトタイプを作ります SwiftUIで新しいビューを作成する時に 考慮したい3つの重要な質問があります ジョブを行うために必要なデータは何か? 今回の例ではブックカバーのサムネイル タイトル 著者名 進捗状況などが必要です ビューはデータをどのように処理するか? この例ではデータを表示するだけで 変更はしません データはどこから来るか? これが“信頼できる情報源”になります 今回の説明ではこのバッジを使って 信頼できる情報源を示していきます この信頼できる情報源に関する質問が データモデルの設計において最も重要です ビューをビルドしながら 考えてみましょう
さて今回のジョブには 何のデータが必要でしょう? bookの値が表紙の画像の名前 タイトル 著者名を示しています progressの値が表すのは進捗状況です 次にビューがデータを どのように処理するかです データを表示するだけなので “let”プロパティにします
そしてデータはどこから来るか? ブックカードがインスタンス化された時に superviewから渡されます 信頼できる情報源は ビュー階層のさらに上の方にあります superviewがブックカードのインスタンスを 作成する時に信頼できる情報源を渡します superviewのbodyが実行する度に 新しいブックカードがインスタンス化されます SwiftUIがレンダリングし終わると インスタンスは消えます 図を使って内部構造を 可視化してみます 左の四角はブックカードのビルドを構成する SwiftUIのビューを表しています 右のカプセルはビューのレンダリングに 使われるデータです セッション全体で同様の図を使います
次は今より少し面白いビューの例です 本をタップすると読書の進捗状況を 確認できる画面が出てきます “Update Progress”ボタンをタップすると 進捗状況を更新したりメモが追加できます こんな感じです
親ビューとこのシートは どのように協働するのでしょうか
シートの表示方法に注目します “Update Progress”ボタンをタップすると presentEditorメソッドが呼び出されます メソッドがstateをいくつか変更し シートが表示されます シートの表示には 何のデータが必要でしょうか? シートが提示されているか追跡するには ブール値が必要です メモの追跡にはStringが 進捗状況の把握にはDoubleが必要です 関連性のあるプロパティが複数ある時には 常に独自の構造体に引き出します
この方法にはBookViewを読みやすくする以外に 2つの優れた点があります カプセル化の利点がすべて得られます EditorConfigのプロパティの不変量は維持され 独立したテストも可能です またEditorConfigは値のtypeなので EditorConfigのプロパティに変更があった場合 EditorConfig自体の変化として 可視化できます 次の質問はデータを どのように処理するかです “Update”ボタンをタップしたら isEditorPresentedを“True”にセットし 進捗を現在の状況に 合わせる必要があります StateをEditorConfig構造体に抽出したので 更新の責任をEditorConfigに持たせられます そしてBookViewに EditorConfigへの作業を依頼するだけです
それから変更メソッドを EditorConfigに追加します こんな感じです
ではデータはどこから来るのか? エディタコンフィギュレーションは このビューではローカルなので データを渡す親ビューがありません そこで信頼できる情報源を ローカルに確立する必要があります SwiftUIの信頼できる情報源で 最も簡単なのがStateです
このプロパティをStateにマークすると SwiftUIがストレージ管理を引き継ぎます 図で見てみましょう ここでも左側のボックスがビューを 右側のカプセルがデータを表しています カプセルが太い線で囲まれているのは SwiftUIが管理しているデータという意味です それがなぜ重要かというと ビューは一時的な存在だからです SwiftUIがレンダリングパスを完了した後には 構造体自体は消えてしまいます しかしStateとしてマークされたプロパティは SwiftUIが管理してくれます 次にこのビューのレンダリングが 必要になった時には 構造体を再構築して 既存のストレージに再接続します
次にProgressEditorを見てみます ProgressEditorが 必要なデータは何でしょう? EditorConfigからのデータすべてです どのようにデータを処理するか? 変更する必要があるのでvarを使います データはどこから来るでしょうか? この場合は興味深い例なので 図に戻ります BookViewとProgressEditorに 注目してください EditorConfigを通常のプロパティとして ProgressEditorに渡す場合を想定します EditorConfigは値のtypeなので Swiftはその値の新しいコピーを作成します ProgressEditorが行った変更はすべて この新しいコピーのみを変更し SwiftUIが管理する オリジナルの値は変更されません つまりProgressEditorがBookViewと 通信することは許可されないのです では次にProgressEditorに 独自のStateプロパティを与えてみます これは私たちが望むもののように思えます SwiftUIに新しいデータの管理を させるということです
悲しいことに これも間違いです Stateは信頼できる情報源を作り出します ProgressEditorによる変更は すべて自分のStateに対してなされ BookViewと共有しているものに ではありません 信頼できる情報源は1つなのです 必要なのはBookViewからの 信頼できる情報源への書き込みアクセスを ProgressEditorと共有する方法です SwiftUIでは書き込みアクセスを共有するために Bindingというツールを使います BookViewがEditorConfigへの Bindingを作成します 既存データへの読み書きを参照し その参照をProgressEditorと共有します
このBindingを通して EditorConfigを更新することで ProgressEditorはBookViewと同じStateを 変更していることになります SwiftUIはEditorConfigへの変更を通知 BookViewとProgressEditorが その値に依存関係を持っていると分かるので 値が変更されると ビューを再レンダリングします
ProgressEditorはBookViewに データを戻す必要があります このタスクにはBindingが適していると お話ししました コールのドル記号は StateからBindingを作成します Stateプロパティラッパーの見積値が Bindingだからです BindingプロパティラッパーはProgressEditorと BookView内のeditorConfigStateとの間に データ依存関係を作成します SwiftUI管理の多くのビルトインは Bindingの取得もします 例えばnoteに使われている 新しいTextEditorコントロールは BindingをString値へ導きます ドル記号の予測値アクセサーを使って EditorConfigの中にあるnoteに 新しいBindingを付けることができます SwiftUIでは既存のBindingから 新しいBindingをビルドできます BindingはStateだけのものではありません
SwiftUIでデータフローを始める際の コツをご紹介しました SwiftUIビューを追加する時には 3つの質問を念頭に置いてください どんなデータが必要か? そのデータをどのように使うのか? そして データはどこから来るか?
変化しないデータには プロパティを使うことができます ビューが所有している過渡的なデータには @Stateを使用します 別なビューが所有している変化するデータには @Bindingを使用します
次はデータモデルの設計について ルカから説明しましょう Stateがすべてではありません ありがとう カート こんにちは 私はルカです カートはStateとBindingを使って UIの変更を促進する方法を説明しました ビューのコードを早く反復処理するには こうしたツールが有効です しかしStateは過渡的なUIステータス用に 設計されたローカルなものです このセクションでは モデルの設計に話を移し SwiftUIが提供する すべてのツールを説明します 一般的にAppのデータの保存と処理には UIとは別のデータモデルが使われています ある重要なポイントに達したら データのライフサイクルを管理したり 副作用の処理や既存のコンポーネントとの 統合が必要になります このような時に使うのが ObservableObjectです まずObservableObjectが どのように定義されるか見てみましょう これはクラス制約のあるプロトコルで 参照型別でのみ適用されます ObjectWillChangeプロパティのみを 要求します ObjectWillChangeはPublisherです その名のとおりObservableObjectプロトコルの 意味的要件として オブジェクトに変更を適用するより前に Publisherが表示されていなくてはなりません デフォルトではPublisherは ボックスから出してすぐに使えます ただ必要ならカスタムして 提供することができます 例えばタイマーに特定のPublisherを使ったり 既存のモデルで見られるKVOPublisherも使えます
プロトコルの次はObservableObjectを 理解するためのメンタルモデルです TypeがObservableObjectに変換される時 皆さんは信頼できる情報源を作成し 変更にどう対応するか SwiftUIに教えています つまりUIのレンダリングに必要なデータを定義し そのロジックを実行しているのです SwiftUIがデータとビューの間にある 依存関係を確立します 図の青いボックスがビューです SwiftUIはこの依存関係を使用して 自動的にビューの一貫性を保ち データを正しく再現します
ObservableObjectを データ依存性サーフェスとして考えてみます これはビューにデータを公開する部分ですが 必ずしも完全なモデルである必要はありません このようにObservableObjectを考えれば 必ずしもデータモデルでなくてもよいのです ストレージやライフサイクルから データを切り離すことができます 値の型を使ってデータをモデル化でき そのライフサイクルと副作用を 参照型で管理できます 例えばデータモデル全体を一元管理する 単一のObservableObjectを持つことができ この図のように すべてのビューで共有ができます こうすれば すべてのロジックを 1つの場所にまとめ App内であり得る状態や変更をすべて 簡単に推論することができます またはデータモデルに特定の投影をもたらす ObservableObjectを複数持つことで Appの一部にフォーカスできます 必要なデータだけ公開するよう 設計されているデータモデルをです これが効果的なのは データモデルが複雑な時に 厳密にスコープされた無効化を Appの一部で提供したい場合です 先ほど構築していたAppに戻り 実際の適用方法を見てみましょう カートが作業していたビューに戻ります これは読書の進捗状況を 更新できるビューで 読書会で面白いと思った事を メモすることもできます
カートはprogress sheetの データに焦点を当てました このビューに必要な残りのデータに注目し ObservableObjectを使って 豊富な機能を追加してみます ディスクへのデータ保存や iCloudとの同期などです
ObservableObjectに準拠した CurrentlyReadingという新しいクラスを作成し 本とその進捗状況を保存します これはデータを表示したり変更に対応するために ビューに公開するモデルの一部です 特定の本は変わることはないので プロパティはletにします 進捗状況を更新すると UIがそれに反応するようにします これは@Publishedプロパティラッパーで progressプロパティに注釈を付けると可能です @Publishedの仕組みを見てみましょう @PublishedはPublisherを公開することで プロパティを観測可能にする プロパティラッパーです ほとんどのモデルでは ObservableObjectに準拠し 変更の可能性があるプロパティに @Publishedを追加するだけで完了です ビューとデータの同期について 心配する必要はありません デフォルトのObservableObject publisherで @Publishedを使う場合 値が変化する直前にシステムが配信し 自動的に無効化が実行されます 高度なユースケースでは @Publishedの予測値はPublisherで 反応性のあるストリームを構築できます
データモデルを設計する際に考慮すべき 3つの質問を再考してみましょう ObservableObjectの場合 2つ目の質問の答えはとても簡単です 常に変化のしやすさを推測します 今度は残りの2つの質問に対する 答えを考えてみましょう モデルは定義したので 次はそれをビューで使う必要があります SwiftUIにはObservableObjectへの 依存関係を作成するために ビューで使用できる プロパティラッパーが3つあります そのツールとは@ObservedObject 今年新しくなった@StateObject そして@EnvironmentObjectです まずはObservedObjectを見てみましょう ObservedObjectは プロパティラッパーで ObservableObjectに準拠したtypeを持つビューの プロパティに注釈を付ける際に使います これを使いビューの依存関係として プロパティの追跡開始をSwiftUIに通知します ビューがジョブを行うのに 必要なデータを定義しています
これは最もシンプルで柔軟性があるツールです ObservedObjectは提供されるインスタンスの 所有権を取得するわけではないので ライフサイクルの管理責任は 皆さんにあります Appでの使い方を見てみましょう BookViewは本とこれまでに記録した 全進捗状況を表示するビューです この画面から読書の進捗状況の 更新もできます currentlyReadingというtypeの プロパティを追加します @ObservedObjectプロパティラッパーで 今作成したtypeに注釈を付けます このプロパティに割り当てられたインスタンスが このビューの信頼できる情報源です これでビュー本体のモデルを 読み込めるようになりました SwiftUIではビューは 常に最新の状態になります 必須のコードもビューを同期するために 管理するイベントもありません データからビューの書き方を 宣言的に書き込みするだけで あとはSwiftUIが全部やってくれます SwiftUIのアップデートライフサイクルについて この後 ラージから詳しく説明しますが その前にSwiftUIが何をしているのか 裏側をお見せします ObservedObjectを使用すると SwiftUIはその特定の@ObservableObjectの objectWillChangeを購読します これでObservableObjectが変更される度に それに依存するすべてのビューが 更新されることになります なぜ更新“される”ではなく “されることになる”なのでしょうか それは SwiftUIは 変更しようとしている時を知り すべての変更を 1回の更新にまとめるからです しかし どう変更するのでしょう? SwiftUIで提供されるUIコンポーネントの多くは データの一部へのBindingを取ります ここまでの説明の中でコンポーネントに 同様の設計をする例をご紹介しました これはSwiftUIの基本的な設計原則の1つです コンポーネントはBindingを受け入れることで データの一部への読み書きアクセスが可能になり 単一の信頼できる情報源も維持できます
StateからBindingを派生する方法を カートがお見せしました 同様のことがObservableObjectでも とても簡単にできます
ObservableObject上の値のtypeのプロパティから Bindingを取得することができます それには変数の前にドル記号の接頭辞を追加し プロパティにアクセスするだけです ブッククラブAppで 具体的に見てみましょう 本の最終章を読み終えると 大きな満足感を得られるので 何か大きな表示でマークを付けたいと思います 思いついたのは“この本を読み終えた”という トグルをタップすることです まず変更を加えるのはモデルです 新しいプロパティの “isFinished”を追加し @Publishedで注釈付けすることで 値が変わればUIに変更が反映されるようにします BookViewがどう変わるか見てみましょう Label付きのToggleを追加して 読み終えたらマークできるようにします Toggleには現在の状況を表示するための Bindingが必要で タップされると値が変更されます currentlyReadingにドル記号の接頭辞を追加し isFinishedプロパティにアクセスするだけで isFinishedにBindingを与えることができます これでユーザーがトグルを操作する度に currentlyReadingが更新され SwiftUIがそれに依存する すべてのビューを更新します 例えばこのビューでは 本を読み終えたら 進捗状況を更新するボタンを無効にしましょう App全体で表示されるブックカバーは ネットワークから非同期にロードさせたいです つまり画面に表示される直前です 高負荷なリソースのため生かすのは ビューが表示されている間だけにしたいのです 一般的にObservableObjectのライフサイクルは ビューに結びつけたいと思うでしょう Stateと同様です ObservedObjectには そのObservableObjectの ライフサイクルがありません そこで より使いやすいツールを用意しました
それが今年新しくなった@StateObjectです @StateObjectでプロパティに注釈を付ける時は 初期値を指定します するとSwiftUIは初回にbodyを実行する直前に その値をインスタンス化します ビューのライフサイクル全体で オブジェクトを存続させるのです これはすごい追加機能です 使い方を実際に見てみましょう 特定の画像を名前でロードするための CoverImageLoaderクラスを作成してみましょう ここでは このクラスのスケルトンを 示しているだけです ObservableObjectにした方法は 示しておらず 画像を保持するプロパティに @Publishedを注釈付けしました 一旦 画像を取得すると SwiftUIは自動的にビューを更新します ビューではCoverImageLoaderを 使うことができるので プロパティに@StateObjectを注釈付けします ビューが作成される時に CoverImageLoaderはインスタンス化されません その代わり bodyが実行される直前に インスタンス化され ビューのライフサイクル全体のために 維持されます あるビューがもはや不要になると SwiftUIは想定どおり CoverImageLoaderをリリースします OnDisappearをいじる必要は もうありません その代わりObservableObjectの ライフタイムを使って リソースを管理することができます SwiftUIでは ビューが非常に低負荷なのです これを主要なカプセル化メカニズムにすることや 理解しやすく再利用しやすい 小さなビューを作成することをお勧めします しかしそうすると最終的にこの図のように 深くて広いビューの階層になるかもしれません これをデータフローの観点から見てみると ビュー階層の上位に ObservableObjectを作成し 同じインスタンスを多くのビューに 渡す必要があるかもしれません 時々ObservableObjectを離れた サブビューで使用する必要があります しかしデータを必要としないビューにも渡して 下すことは面倒であり 多くのボイラプレートを 巻き込んでしまうことがあります 幸いなことにこの問題を解決する方法が EnvironmentObjectにあります EnvironmentObjectはビュー修飾子でもあり 同時にプロパティラッパーでもあります ObservableObjectを投入したい親ビューで そのビュー修飾子を使用します そして特定ObservableObjectのインスタンスを 読みたいすべてのビューで プロパティラッパーを使用します フレームワークは必要な場所でその値を渡し それが読み込まれた場所だけ 依存関係として追跡します このパートで見てきたのは ObservableObjectでモデルを設計する方法や SwiftUIであなたの要求に沿うアーキテクチャを ビルドするための基礎的primitiveでした
またビューとデータの一部の間に 依存関係を作成する方法や 新しい@StateObject プロパティラッパーを使って @ObservableObjectのライフサイクルを ビューに結びつける方法を見てきました そして最後は@EnvironmentObjectを使った ビュー階層内にObservableObjectを流すのに 便利な方法です ではラージに引き継ぎます SwiftUIのビューのライフサイクルと パフォーマンスとの関係性や 副作用への対処法と新しいツールを 深く掘り下げてくれます ありがとうございました 素敵なWWDCをお楽しみください ありがとう ルカ SwiftUIでデータを使い始める方法と すばらしいモデルを設計する方法が 分かったかと思います さて私からは最高のパフォーマンスを 発揮するための設計方法と 信頼できる情報源を選ぶ際 さらに考慮すべきことをお話します まず始めに SwiftUIシステムにおけるビューと その役割についてお話しましょう ビューは単純にUIの一部の定義です SwiftUIはこの定義を使って 適切なレンダリングを作成します SwiftUIはビューのIDと ライフタイムを管理します そしてビューはUIの一部を定義するため 軽量で安いものでなければなりません ブッククラブAppでは 他のAppと同様に 画面上のすべてのものがビューです ビューのライフタイムは それを定義する構造体のライフタイムと 別物であることに注目してください Viewプロトコルに準拠して作成した構造体は 実はライフタイムが非常に短いのです SwiftUIがレンダリングを作成したら それはなくなってしまいます ビューを軽量化することの重要性について 図を使って説明してみましょう
この図はSwiftUIの 更新ライフサイクルを示しています 一番上のUIからはじめます 反時計回りに移動してあるイベントが発生すると 何らかのクロージャやアクションが実行されます その結果信頼できる情報源の 変更が起こります
そうして一旦 信頼できる情報源を 変更してから ビューの新しいコピーを取得し レンダリングに使用します このレンダリングがUIです これが基本です この図は少し後でまたお見せします 今ここでは単純化してみましょう そして もう少し だいぶいいですね
その名の通り このライフサイクルは円です Appの実行中は何度も繰り返します このサイクルはパフォーマンスにとっても 本当に重要です そして理想ではこのようにサイクルが スムーズに進むのです
しかしこれらのポイントで 高い負荷のブロッキング作業が行われていると Appのパフォーマンスが低下してしまいます
私たちはこれを スローアップデートと呼んでいます つまりフレームを落としたり Appがハングしたりする可能性があります スローアップデートを避けるには どうしたらいいでしょうか?
スローアップデートを避けるために 最初にできることの一つとして ビューの初期化の負荷を楽にできるか確認します ビューの本体は副作用のない 純粋な関数でなければなりません つまりビューの記述を作成して 返すだけでよいのです ディスパッチや他の作業は必要ありません ただいくつかビューを作成して 次に進みましょう bodyがいつどのくらいの頻度で呼び出されるかの 仮定条件の作成は避けましょう SwiftUIは時々かなり賢く 仮定があるとそれに伴って bodyを呼び出さないかもしれません
Bodyの負荷が軽いことの重要性を図示するので 例を見てみましょう
これはCurrently Readingのリストが どのように見えるかを単純化した例です ReadingListViewerでは bodyをNavigationViewとして定義し その中にReadingListを入れます それからReadingListで @ObservedObjectを使ってデータを生成します かなり理にかなって見えますが 実はここにバグが潜んでいます お分かりですか?
ReadingListViewerのbodyが 作成される度 ReadingListStoreの割り当てが 何度も繰り返されることが分かります これがスローアップデートの原因かもしれません ビュー構造体には定義されたライフタイムがない ということを思い出してください これを書くと 実際には毎回オブジェクトの 新しいコピーを作成することになってしまいます 割り当てが何度も繰り返されるのは 負荷も時間もかかります 毎回オブジェクトがリセットされるため データロスも発生します 問題が判明しましたが どうやって解決すればいいのでしょうか? 今まではどこか別の場所でモデルを作成して 渡す必要がありました しかしインラインで行うことができたら すばらしいのでは? 今年はこの問題を解決するツールである @StateObjectを紹介できます そしてルカが先ほど説明したように @StateObjectはSwiftUIがObservableObjectを 正しい時にインスタンス化できるようにします そのため不必要に多くの割り当てが発生せず またデータを失うこともありません つまりすばらしいパフォーマンスと 正確性を得ることができるということです ここで新たな信頼できる情報源を 宣言しているので そのバッジを貼っておきましょう ではまた図を見てみましょう そして高度にします いいですね 注目すべき点は左上にイベントの 複数のソースがあることです Appがボタンのタップなどのユーザーの 相互作用で生成されたイベントに反応したり あるいはタイマーや通知といった 配信の停止に反応したりするかもしれません イベントのトリガーに関係なく サイクルは同じです 信頼できる情報源を変更させ ビューを更新し 新しいレンダリングを生成します これらのトリガーはイベントソースと呼ばれ これらはビューの変化を加速させます 先ほども述べたようにユーザーの相互作用や タイマーなどのパブリッシャーがよい例です そして今年は新しく環境やbindingの変更などの イベントに反応したり URLやユーザーアクティビティを 扱う新しい方法があります これらの修飾子それぞれがパブリッシャーや 比較値などのパラメータを取るほか クロージャも取ります SwiftUIは適切なタイミングで クロージャを実行して スローアップデートを回避し bodyの負荷を安く保つのに役立ちます しかしSwiftUIはこれらのクロージャを メインスレッド上で実行します そのため高負荷な作業が必要な場合は バックグラウンドキューへのディスパッチを 検討してみてください 詳細を知りたい方は ドキュメントでの確認をお勧めします 以上がスローアップデートを回避する方法と 優れたパフォーマンスの実現のための 設計方法でした 次に例の3つの質問に戻りましょう
最初の2つの質問は大体説明しましたが 3つ目は簡単ではありません この質問に答える時 あなたは使用する 信頼できる情報源を決定していることになります これは難しい質問です ここに正解はありませんので 手助けとして 考慮すべきことを説明したいと思います
重要な問題は誰がデータを 所有しているのかということです データを使用しているのはビューなのか それともそのビューの祖先なのでしょうか もしかしたらAppの別の サブシステムかもしれません
ここにガイドラインがあります 前述したように共通の祖先と データを共有すべきです またObservableObject のビュー所有の 信頼できる情報源として @StateObject をうまく使うことです 最後に App内にグローバルデータを 配置することを考えてください これについては次で説明します
次に データのライフタイムについて 説明します データのライフタイムをViewsやScenesやAppsに 結びつけることについてです SwiftUIのシーンとAppについては “App Essentials in SwiftUI”の セッションを参照してください まずViewsから始めましょう
この話でご覧になった通り ビューはデータのライフタイムを 関連付けるためのすばらしいツールです そして今日 説明したすべての プロパティラッパーはビューで動きます StateとStateObjectの プロパティラッパーを使用して データのライフタイムと ビューのライフタイムを結びつけられます 次にscenesについて説明しましょう SwiftUIのscenesはそれぞれ独自の ビューツリーを持っているので ツリーのルートから重要なデータを ぶら下げることができます 例えばウィンドウグループ内のビューに 信頼できる情報源を置くことができます そうすることで ウィンドウグループが作成する sceneの各インスタンスは それ自身の独立した信頼できる 情報源を持つことができます そして基礎となるデータモデルの レンズとして機能します これは複数のウィンドウを 使用した場合に効果的です ご覧のように2つのscenesは同じ基礎となる データを表す独立したUIのstatesを持ちます これでルカは2つのオンラインブッククラブを 同時に行くことができるようになりました 次にAppsについて話しましょう Appsは今年のSwiftUIの強力な新しいツールで SwiftUIだけでApp全体を書くことができます しかしAppsのすばらしい点は ビューで行うのと同じように App内でStateや他の信頼できる 情報源を使用できることです 例をお見せしましょう @StateObjectプロパティラッパーを使って App全体のグローバルブックモデルを作成します 変更される度にAppのすべてのscenesが更新され 結果的にすべてのビューが更新されます そしてこれを書くことでApp全体の 信頼できる情報源を作成しました 信頼できる情報源の 置く場所を検討する時 本当のグローバルデータを表示する場合は App内に置くとよいでしょう
データのライフタイムは重要です しかしそのライフタイムはその信頼できる 情報源のライフタイムと結びついており 私たちはStateやStateObjectや Constantのようなツールを使用してきました しかしこれらのツールにはプロセスの ライフタイムに縛られるという制限があります つまりAppが強制終了したり デバイスが再起動したりした場合 Stateは戻ってきません この問題を解決するために 今年はストレージを導入しました ライフタイムを延長しかつ自動的に 保存および復元できるものです ただしこれらはモデルには該当しません むしろ モデルと一緒に使う 軽量な貯蔵庫です
SceneStorageから見始めていきましょう
SceneStorageはsceneごとに スコープされたプロパティラッパーで SwiftUIによって完全に管理された データの読み書きを行います ビュー内からのみアクセス可能なので Appの現在の状態に関する軽量な情報を 保存するのに最適な候補となります 例を見てみましょう
これは先ほどのブッククラブAppで 2つのシーンを横に並べたものです SceneStorageを使用する際に 最初に考えるべきことは 本当に復元が必要かということです 今回の場合 保存と復元に 必要なのは選択範囲だけです ブックの名前や進捗状況 ノートはすべてモデルに蓄積されているので それらを保存して 復元する必要はありません では コードに移りましょう 選択したStateの保存や復元のために SceneStorageを使用する方法です 保存しているデータの種類に合わせて 必ず固有のキーを渡します そうするとStateと 同じように使うことができます SwiftUIが私たちの代わりに自動的に 値を保存して復元してくれます これはStateと似た挙動をするので 今回は事実上 すでに新しい信頼できる 情報源をここに宣言します そのためこれがシーン全体の 信頼できる情報源となります これでデバイスの再起動や システムがある シーンを保存しているリソースを取り戻す時も 次の起動時にデータを 利用できるようになります これでルカはブッククラブの活動を 前に閉じたところから取り出すことが可能です さてSceneStorageの動作の後は AppStorage についても説明したいと思います これはAppに焦点をあてた グローバルストレージで ユーザーのデフォルトを使って 保持されるものです どこからでも利用できるので App内からでも ビュー内からでもアクセスできます AppStorageはユーザーデフォルトと同様に 設定などの小さなデータを 保存するのに非常に便利です 私たちのブッククラブAppでも設定を いくつか導入することにしました これを達成するために AppStorageを 使用する方法を見てみましょう 設定ビューでは AppStorageプロパティラッパーを追加して キーを与えるだけです デフォルトでは標準ユーザデフォルトを 使用していますが 必要に応じてカスタマイズすることができます 詳細については ドキュメントを参照してください この場合 ここでは2つの設定の それぞれに対してAppStorageを設定します これらは独立したデータなので それぞれにユニークなキーを与えます SceneStorageと AppStorageを使用する際には データ固有の部分に 固有なキーを持たせることが重要です そして他のプロパティラッパーと同様に 信頼できる情報源でもあり Toggleで使用するためのbindingを 得られることを意味します これでデータが変わる度に 自動的に保存され ユーザデフォルトから 復元されるようになりました 今日はプロセスのライフタイムと ライフタイムの延長に使えるツールを見ました 各ツールを使って ここでUIレベルの Stateを蓄積します ストレージのtypeについては 永続性が無限なわけではないので 何でも一緒に蓄積しないようにしてください もう1つ ObservableObjectというツールを 紹介したいと思います ObservableObjectは 非常に柔軟に設計されているので これを使って非常にカスタムしやすい 挙動を実現することができ サーバや他のサービスでの データバックアップなどが可能です これらのツールを組み合わせることで ニーズに最も適したものを選択する 自由度の高いファミリーを形成しています 今日は多くのツールと その使用法を見てきましたが 主な残課題とは 1つだけで万能なツールは 存在しないということです それぞれに特徴があるので 典型的なAppは さまざまなツールを使用しているわけです 私たちがブッククラブAppで それを実行してきましたね ボタンと進捗状況ビューには Stateを使用していますが Currently ReadingモデルにはStateObject 選択範囲にはSceneStorage 設定にはAppStorageを使用しています データのプロパティや正しく信頼できる 情報源は何かと考えることが重要です そして煩雑さを減らすために 信頼できる情報源の数を制限してみてください 最後に 再利用可能なコンポーネントを ビルドする方法としてbindingを活用しましょう bindingは信頼できる情報源に 全く依存しないので クリーンな抽象化をビルドするための 強力なツールとなることを覚えていてください 学んだ要点を生かし すばらしいモデルをビルドしてください ありがとうございました
すばらしいWWDCをお楽しみください
-
-
2:09 - BookCard
struct BookCard : View { let book: Book let progress: Double var body: some View { HStack { Cover(book.coverName) VStack(alignment: .leading) { TitleText(book.title) AuthorText(book.author) } Spacer() RingProgressView(value: progress) } } }
-
3:35 - EditorConfig
struct EditorConfig { var isEditorPresented = false var note = "" var progress: Double = 0 mutating func present(initialProgress: Double) { progress = initialProgress note = "" isEditorPresented = true } } struct BookView: View { @State private var editorConfig = EditorConfig() func presentEditor() { editorConfig.present(…) } var body: some View { … Button(action: presentEditor) { … } … } }
-
5:59 - ProgressEditor
struct EditorConfig { var isEditorPresented = false var note = "" var progress: Double = 0 } struct BookView: View { @State private var editorConfig = EditorConfig() var body: some View { … ProgressEditor(editorConfig: $editorConfig) … } } struct ProgressEditor: View { @Binding var editorConfig: EditorConfig … TextEditor($editorConfig.note) … }
-
13:15 - CurrentlyReading
/// The current reading progress for a specific book. class CurrentlyReading: ObservableObject { let book: Book @Published var progress: ReadingProgress // … } struct ReadingProgress { struct Entry : Identifiable { let id: UUID let progress: Double let time: Date let note: String? } var entries: [Entry] }
-
15:36 - BookView
struct BookView: View { @ObservedObject var currentlyReading: CurrentlyReading var body: some View { VStack { BookCard( currentlyReading: currentlyReading) //… ProgressDetailsList( progress: currentlyReading.progress) } } }
-
17:50 - CurrentlyReading with isFinished
class CurrentlyReading: ObservableObject { let book: Book @Published var progress = ReadingProgress() @Published var isFinished = false var currentProgress: Double { isFinished ? 1.0 : progress.progress } }
-
18:21 - BookView with Toggle
struct BookView: View { @ObservedObject var currentlyReading: CurrentlyReading var body: some View { VStack { BookCard( currentlyReading: currentlyReading) HStack { Button(action: presentEditor) { /* … */ } .disabled(currentlyReading.isFinished) Toggle( isOn: $currentlyReading.isFinished ) { Label( "I'm Done", systemImage: "checkmark.circle.fill") } } //… } } }
-
19:58 - CoverImageLoader
class CoverImageLoader: ObservableObject { @Published public private(set) var image: Image? = nil func load(_ name: String) { // … } func cancel() { // … } deinit { cancel() } }
-
20:20 - BookCoverView
struct BookCoverView: View { @StateObject var loader = CoverImageLoader() var coverName: String var size: CGFloat var body: some View { CoverImage(loader.image, size: size) .onAppear { loader.load(coverName) } } }
-
25:36 - ReadingListViewer (Bad)
struct ReadingListViewer: View { var body: some View { NavigationView { ReadingList() Placeholder() } } } struct ReadingList: View { @ObservedObject var store = ReadingListStore() var body: some View { // ... } }
-
26:39 - ReadingListViewer (Good)
struct ReadingListViewer: View { var body: some View { NavigationView { ReadingList() Placeholder() } } } struct ReadingList: View { @StateObject var store = ReadingListStore() var body: some View { // ... } }
-
30:52 - App-wide Source of Truth
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } }
-
32:43 - SceneStorage
struct ReadingListViewer: View { @SceneStorage("selection") var selection: String? var body: some View { NavigationView { ReadingList(selection: $selection) BookDetailPlaceholder() } } }
-
33:49 - AppStorage
struct BookClubSettings: View { @AppStorage("updateArtwork") private var updateArtwork = true @AppStorage("syncProgress") private var syncProgress = true var body: some View { Form { Toggle(isOn: $updateArtwork) { //... } Toggle(isOn: $syncProgress) { //... } } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。