ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIでの並行処理
Swiftの並行処理機能を使用して、さらに優れたSwiftUI Appを構築する方法を確認しましょう。並行処理のワークフローがObservableObjectsとどのように相互作用するかを示し、SwiftUIのビューとモデルで直接使用する方法を探ります。SwiftUIのランループ上でAppをスムーズに動作させるawaitの使い方を確認し、AsyncImage APIを使用してリモート画像を素早く取得する方法を確認します。また、カスタムビューで追加の非同期フローを有効にするプロセスも紹介します。
リソース
関連ビデオ
WWDC22
WWDC21
- MusicKit for Swiftについて
- Swiftにおける構造化並行処理
- Swiftのasync/awaitについて
- Swiftアクターによるミュータブルステートの保護
- SwiftUIの徹底解説
- SwiftUIの新機能
WWDC20
-
ダウンロード
「SwiftUIでの並行処理」 SwiftUIチームのCurt Cliftonです 同僚のJessicaも後で参加します Swift 5.5はSwiftのコードで 並行処理を管理するための 様々なツールを導入しました 今回の改善は Swift Appに どのように作用するのでしょうか 新しいツールがデータモデルを さらに改善するのにどのように役立つのか SwiftUIがどのように機能するか説明します 次に データモデルをSwiftUIビューに 接続する方法を示し SwiftUIの新しい 並行処理ツールを利用する 優れた最新のAPIを紹介します これからお話しする 情報を最大限に活用するには Swiftの新しい 並行処理サポートの背景を 理解することが大切です このビデオを観る前に 「Swiftのasync/awaitについて」と 「Swiftにおける構造化並行処理」 の動画をご覧ください 子供のころ いつも宇宙飛行士に なりたいと夢見てました いつか宇宙船で仕事をしたい 残念ながら 幼少期の夢が 叶うことはありませんでした しかし 今でも 宇宙への熱意は変わりません そこでSwiftUIのエンジニアとして 自分のスキルを活かし 宇宙関連の画像をダウンロードします 計画しているAppをご覧ください 様々な宇宙の画像が並んでいます 美しい色彩ですね お気に入りの画像を見つけて 後で見るために保存できます 美しい画像を取得するため 私のAppはREST APIを使用して Webサービスと対話します Swiftの 新しい 並行処理機能を使うのに最適です データモデルから始めましょう SpacePhoto structを使って ある写真の情報を得ます この構造体にはタイトルや 写真の説明 投稿された日付 実際の画像のURLなどの フィールドがあります サーバーの応答から 簡単にインスタンス化できるように Codableにしました そしてディスクに保存し識別可能にして データ駆動型ビューで 使用できるようにします 次にこれらのエントリの リストを表示します そのためにはコレクションを取得して 保持するモデルが必要です これにはPhotosクラスを使用します Photosクラスを ObservableObject に 準拠させることにより データが更新されるたびに SwiftUIビューが自動的に更新されます 公開されたプロパティを使用して SpacePhotosの配列を格納します
RESTエンドポイントから 更新アイテムを得るために UpdateItemsメソッドを使います 後でもっと詳しく説明します まず 基本的なユーザー・ インターフェースについて
これは私が構築したいUIです これまでのところタブビューと 基本的な PhotoViewがあります
PhotoViewは 宇宙の写真と タイトルを表示します データモデルの動作を確認するのに これで十分です 次に CatalogViewで見てみましょう CatalogViewに写真のリストが 表示されます これには StateObject を追加して PhotosのObservable Objectを使って インスタンス化します Viewの本体に NavigationViewを追加します NavigationViewを使うと すぐに大きなナビゲーション タイトルを追加できます 次に NavigationViewにリストを追加します リストにはForEachを使って 写真をマッピングして それぞれの写真にPhotoViewを表示します
これでサンプルデータを見られます
基本の説明はここまでですが もう少し磨きをかけてみましょう
最初にナビゲーションのタイトルです デフォルトのインセットリストも 見栄えがしますが 宇宙の写真をより際立たせるために Plain Styleに切り替えて 黒の背景に写真が 映えるようにしたいと思います
新しいenumのstaticメンバー構文で listStyleをプレーンにします この構文ではSwiftUIのスタイル修飾子は Xcode 13の自動補完サポートが向上して サポートが向上し より簡潔なスペルになります 最後に 今年追加された 新機能であるリストセパレーターの 制御を使用します
ForEach内で listRowSeparator修飾子を使用して 区切り文字を非表示にできます
ユーザーインターフェイスを 磨き上げるのは いくらでも時間を費やせるので この辺で終わりにします 私がデータモデルを完成させたら Jessicaが仕上げてくれる計画です データモデルを掘り下げる前に SwiftUIがObservableObjectと どのように相互作用するか 少しお話しします Swift5.5の新しい並行処理機能により この相互作用をこれまで以上に 簡単に実現できるようになりました 「Swift UIにおけるデータの重要事項」で Rajが 更新ライフサイクルについて 話しました このライフサイクルを推進するコードを 「実行ループ」と呼びます 実行ループはMainActorで実行されます アクター全般の詳細については 「Swiftアクターによる ミュータブルステートの保護」 という動画をご覧ください 僕たちはMainActorに焦点を絞ります SwiftUI実行ループはイベントを受信し モデルを更新してから SwiftUIビューを 画面にレンダリングします これらの更新を 「実行ループの目盛り」と呼びます 展開すると 複数のティックを 連続して見られます
ObservableObjectsは いくつかの興味深い方法で SwiftUI実行ループと対話できます Photos ObservableObject に戻り updateItemsメソッドを見てみましょう SwiftUIビューから updateItemsを呼び出すと MainActorで実行されます この青い長方形を使用してupdateItemsが 実行されている時間を示しましょう フェッチした写真を itemsプロパティに割り当てる このコード行に焦点を当てたいと思います "items"はPublishedのプロパティなので この割り当てによって objectWillChangeイベントが発生して すぐにフェッチされた写真が "items"のストレージに書き込まれます SwiftUIがobjectWillChangeを確認して itemsのスナップショットを取得します スナップショット後の 実行ループの次のティックで SwiftUIはスナップショットを 現在の値と比較します これらの値が異なるため SwiftUIは Photosに依存する ビューを更新することを認識しています objectWillChange ストレージの更新 および実行ループティックは すべてMainActorで発生するため 順番に発生することに注意してください 2020年の「Data Essentials」で ビューが本体で機能しすぎると 更新が遅くなるという説明があります
同様にモデルコードがMainActorに対して 作業を行いすぎた場合も遅くなります
たとえば ダウンロードが完了するのを 待っている間に fetchPhotos関数がブロックされ 接続速度が遅くなったとします 私はMainActorをブロックしているので 実行ループのこのティックを見逃します ユーザーには遅延に見えます 以前は 作業を実行するため 別のキューにディスパッチした 可能性があるため 高価なfetchPhotos が メインスレッドから発生します 一見大丈夫そうだが 厄介な問題を抱えています ObservableObjectを MainActorから変更したので 変更と実行ループティックが インターリーブする可能性があります たとえばアイテムに割り当てたときに SwiftUIがobjectWillChange スナップショットを取得すると 実行ループのティックの直前に 発生する可能性があります 状態の変化はまだ起こっていないため SwiftUIはスナップショットを 変更されていない値と比較します 実際の状態の変化は、 実行ループティックの後に 発生しますが SwiftUIは その変化を認識しないため ビューは更新されません 正しく更新するには 次のイベントが順番に 発生する必要があります objectWillChangeと ObservableObjectの状態が更新され 実行ループが次のティックに到達します これらすべてが MainActorで発生することを 確認できれば この順序を保証できます Swift 5.5以前は 状態を更新するためにメインキューに ディスパッチしていたかもしれません 今でははるかに簡単になっています awaitを使いましょう! awaitを使用してMainActorから 非同期呼び出しを行うことで MainActorで他の作業を続けられます これはMainActorのyeildと呼ばれます
updateItemsではawaitを使用して 長時間実行しているI / O中に MainActorをSwiftUIに戻すことが できるため 実行ループを維持して UIの不具合を避けることができます 非同期作業が完了すると SwiftはMainActorで updateItemsメソッドを 再入力するため 状態を更新できます どのように機能するか見てみましょう 別のキューにディスパッチする代わりに 長時間実行された 操作の結果を待つだけです 私がawaitと書くと updateItems関数が 実行ループを続行できるように MainActorの制御を生成します 待機中のフェッチが完了すると MainActorが関数に再入力します 公開されたプロパティを安全に更新し objectWillChangeをトリガーして 新しい値をSwiftUIで 使用できるようになります Xcodeにジャンプして フェッチを実行します updateItemsメソッドは次のとおりです fetchPhotosを実行するには 写真をフェッチするコードを 追加してみましょう fetchPhotoメソッドに 残りのエンドポイントから 写真のURLを取得させ SpacePhotoを返します
URLSessionの便利なdataメソッドの 新しい非同期バージョンを使って URLからデータを取得してみます
これをスタブ化するために 強制的な試行を行っています 後ほど説明します
dataメソッドは非同期なので awaitを使用します
つまり fetchPhotoメソッドを 非同期にする必要があります
できました データを取得したので Decodableイニシャライザを使用して 写真をインスタンス化し返します
次にfetchPhotosを見てみましょう 日付をランダムに選択して ループするために いくつかのコードをスタブしました 配列を作成したいので ダウンロードした変数を作成し 日付変数をループに追加します
ループ内では特定の日付を取得するための RESTエンドポイントの URLを構築するために ヘルパーメソッドを呼び出します
FetchPhotoメソッドを呼び出して 結果を配列に追加します
作ってみましょう FetchPhotoは非同期なので 結果を待つ必要があります
つまりfetchPhotosは 非同期である必要があります
簡単にするためにFetchPhotoの呼び出しを 連続して行っています Swift 5.5のタスクグループには さらに強力なオプションが 用意されています スライドで示したように fetchPhotosを待つだけです
これで更新ロジックが整いました フェッチを実現するための強制的な試みに 不安を感じるかもしれません 整理しましょう ダウンロードに失敗したら nilをリターンします
そして fetchPhotosでは nilでない値だけを配列に追加します
Photosはasync-awaitを使用しているので MainActor上で実行する限り
厄介なobjectWillChangeのバグに 遭遇しないことを確信できます どうしたら確実でしょうか? Swiftのコンパイラが助けてくれます 新しい@MainActorアノテーションを Photosに追加することにより コンパイラは Photosの プロパティとメソッドが MainActorからのみ アクセスされることを保証します それが終わるとモデルが完成します 次にJessicaがビューをモデルに接続し Appで並行処理を活用するための 新しいSwiftUI APIを紹介します Jessica
Curt ありがとう CatalogViewに切り替えましょう Curtが見せてくれた updateItemsメソッドを使用します
カタログが表示されるたびに updateItemsを呼び出す時 以前はonAppearを 使用したかもしれません 今年からSwiftUIで タスク修飾子を使用します Taskを使用すると非同期タスクを ビューに関連付けることができます タスクは ビューのライフタイムの 最初に開始されます タスクはデフォルトで非同期であるため そのクロージャ内で myPhotosオブジェクトで updateItemsを呼び出して 結果を待つことができます
これはタスクの優れた使用法ですが この新しい修飾子はさらに活用できます タスクの有効期間はビューの寿命に 関連付けられているため 非同期シーケンスを待機したり その値に応答することができます また ビューの有効期間が終了すると タスクは自動的にキャンセルされます ビューの有効期間の詳細については 「SwiftUIの徹底解説」 を確認してください ライブプレビューを使うと エントリが更新されています しかし まだ美しい画像が欠けています Curtが見せてくれたPhotoViewは すでに更新されています タイトルの後ろに背景を追加します それでは画像を追加しましょう 新しいAsyncImage APIを使用すると リモートサーバーからの画像の読み込みが これまでになく簡単になります 私がしなければならないのは エントリからフェッチしたい 画像のURLを取得し それをAsyncImageに渡すことだけです
フルサイズでは少し大きすぎるので AsyncImageのオーバーロードを使用して 画像を調整しプレースホルダーを表示して ユーザーに画像を読み込み中と知らせます
次に画像のサイズを変更し ペースを埋めるように アスペクト比を設定します
最後に幅と高さの最小値を追加して 画像に柔軟性を持たせます 最小の高さをゼロ以外にすることで ProgressViewがタイトル領域から 顔を出します
SwiftUIの他の部分と同様にAsyncImageは デフォルトで構築されているので 画像の読み込み中に エラーが発生した場合でも プレースホルダーが引き続き表示されます エラー処理の動作をカスタマイズできます これを行うには 「AsyncImage」「AsyncImagePhase」 をご覧ください また ユーザーが お気に入りの画像を保存して 後で表示できると便利です このタイトル領域に ボタンを追加しましょう ボタンは非同期アクションをトリガーして イメージエントリをディスクに保存します そして Appの保存済みタブに表示されます これを行うためのビューを すでにスタブしています ここに追加してコードを見てみましょう
保存ボタンのスタブインバージョンです 写真を保存するアクションを 追加しましょう SwiftUIのボタン操作は同期的ですが 私の保存メソッドは非同期的です メソッドを呼び出すために 非同期タスクを起動します
次に クロージャー内で 写真の保存メソッドを呼び出します 非同期なのでawaitを使用します
保存中に ProgressViewを表示すると 良いと思います そのために @State プロパティを 追加します
保存するための @Stateを更新していきます
ボタンのラベルを更新して 保存が行われているときに ProgressViewを表示します opacityを使って「保存」ラベルを隠し オーバーレイ を使って ProgressViewを表示します この組み合わせにより 「保存」という単語の ローカリゼーションに基づき ボタンのサイズを保ちます
最後に 保存中はボタンを無効にします
ライブプレビューで見てみましょう
素晴らしいです! カタログビューへ戻って総括します
SwiftUIには今年 素晴らしい 修飾子が追加され ユーザーがデータを手動で更新する機能を 提供できるようになりました Listに refreshable修飾子を追加することで このコンテンツは更新可能と通知します refreshableに 非同期のクロージャを提供し updateItemsメソッドを 呼び出してListを更新します taskで示したように この非同期メソッドにはawaitを使います
非同期の作業が終了すると リフレッシュ・インジケータは 自動的に解除されます これで プルダウンして画像を更新したり 「保存」をタップして 気に入った画像を保存したり 保存タブに切り替えて画像を確認できます
Swiftの新機能により データを簡単に並行処理できます SwiftUIは Swiftの並行処理機能をうまく統合し デフォルトで最高の動作を提供します 多くの場合 awaitを使用して 並行処理機能を利用します ObservableObjectに @MainActorをマークすると オブジェクトがビューと うまく連動して更新されるか 厳密にチェックすることができます
SwiftUIの追加APIを利用して 安全でパフォーマンスの高い 並行処理Appを 最小限の労力で書くことができます AsyncImageを使用して 画像を同時に読み込みます refreshable修飾子をビュー階層に追加して データを手動で更新します 保存ボタンで説明したように Swiftの新しい並行処理機能を 独自のカスタムビューで 使用することができます
並行処理は厄介なものです 難しい問題ですが これらの新しい言語機能と SwiftUI APIを使用すると Appの複雑さを管理するための ツールを利用できるようになります Swift 5.5とSwiftUIに新しく搭載された 素晴らしい並行処理ツールについて 楽しく学んでいただけたと思います 皆さんがAppの 厄介な問題に取り組むために これらのツールがどのように 使用されるのか楽しみにしています [音楽]
-
-
1:55 - SpacePhoto
/// A SpacePhoto contains information about a single day's photo record /// including its date, a title, description, etc. struct SpacePhoto { /// The title of the astronomical photo. var title: String /// A description of the astronomical photo. var description: String /// The date the given entry was added to the catalog. var date: Date /// A link to the image contained within the entry. var url: URL } extension SpacePhoto: Codable { enum CodingKeys: String, CodingKey { case title case description = "explanation" case date case url } init(data: Data) throws { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(SpacePhoto.dateFormatter) self = try JSONDecoder() .decode(SpacePhoto.self, from: data) } } extension SpacePhoto: Identifiable { var id: Date { date } } extension SpacePhoto { static let urlTemplate = "https://example.com/photos" static let dateFormat = "yyyy-MM-dd" static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.dateFormat = Self.dateFormat return formatter } static func requestFor(date: Date) -> URL { let dateString = SpacePhoto.dateFormatter.string(from: date) return URL(string: "\(SpacePhoto.urlTemplate)&date=\(dateString)")! } private static func parseDate( fromContainer container: KeyedDecodingContainer<CodingKeys> ) throws -> Date { let dateString = try container.decode(String.self, forKey: .date) guard let result = dateFormatter.date(from: dateString) else { throw DecodingError.dataCorruptedError( forKey: .date, in: container, debugDescription: "Invalid date format") } return result } private var dateString: String { Self.dateFormatter.string(from: date) } } extension SpacePhoto { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) description = try container.decode(String.self, forKey: .description) date = try Self.parseDate(fromContainer: container) url = try container.decode(URL.self, forKey: .url) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(description, forKey: .description) try container.encode(dateString, forKey: .date) } }
-
2:39 - Photos
/// The current collection of space photos. class Photos: ObservableObject { @Published private(set) var items: [SpacePhoto] = [] /// Updates `items` to a new, random list of photos. func updateItems() async { let fetched = fetchPhotos() items = fetched } /// Fetches a new, random list of photos. func fetchPhotos() -> [SpacePhoto] { let downloaded: [SpacePhoto] = [] for _ in randomPhotoDates() { } return downloaded } }
-
3:24 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) } } }
-
10:09 - Make fetch happen
/// An observable object representing a random list of space photos. @MainActor class Photos: ObservableObject { @Published private(set) var items: [SpacePhoto] = [] /// Updates `items` to a new, random list of `SpacePhoto`. func updateItems() async { let fetched = await fetchPhotos() items = fetched } /// Fetches a new, random list of `SpacePhoto`. func fetchPhotos() async -> [SpacePhoto] { var downloaded: [SpacePhoto] = [] for date in randomPhotoDates() { let url = SpacePhoto.requestFor(date: date) if let photo = await fetchPhoto(from: url) { downloaded.append(photo) } } return downloaded } /// Fetches a `SpacePhoto` from the given `URL`. func fetchPhoto(from url: URL) async -> SpacePhoto? { do { let (data, _) = try await URLSession.shared.data(from: url) return try SpacePhoto(data: data) } catch { return nil } } }
-
14:07 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) .refreshable { await photos.updateItems() } } .task { await photos.updateItems() } } }
-
15:11 - PhotoView with image
struct PhotoView: View { var photo: SpacePhoto var body: some View { ZStack(alignment: .bottom) { AsyncImage(url: photo.url) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { ProgressView() } .frame(minWidth: 0, minHeight: 400) HStack { Text(photo.title) Spacer() SavePhotoButton(photo: photo) } .padding() .background(.thinMaterial) } .background(.thickMaterial) .mask(RoundedRectangle(cornerRadius: 16)) .padding(.bottom, 8) } }
-
18:06 - SavePhotoButton
struct SavePhotoButton: View { var photo: SpacePhoto @State private var isSaving = false var body: some View { Button { Task { isSaving = true await photo.save() isSaving = false } } label: { Text("Save") .opacity(isSaving ? 0 : 1) .overlay { if isSaving { ProgressView() } } } .disabled(isSaving) .buttonStyle(.bordered) } }
-
20:28 - CatalogView
struct CatalogView: View { @StateObject private var photos = Photos() var body: some View { NavigationView { List { ForEach(photos.items) { item in PhotoView(photo: item) .listRowSeparator(.hidden) } } .navigationTitle("Catalog") .listStyle(.plain) .refreshable { await photos.updateItems() } } .task { await photos.updateItems() } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。