ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
超高速なリストとコレクションビューの構築
スムーズなスクロール型リストおよびコレクションビューの構築: セルのライフサイクルを確認し、その知識を応用して荒いスクロール、フレームの欠落をなくす方法を確認します。また、画像読み込みの最適化やセルの自動プリフェッチにより、全体的なスクロール体験を向上させ、コストのかかる不具合を回避する方法も紹介します このビデオを最大限に活かしていただくためには、diffableデータソースとコンポジションレイアウトの基礎を理解していることが推奨されます。
リソース
関連ビデオ
WWDC21
WWDC20
Tech Talks
WWDC16
-
ダウンロード
♪ ♪ こんにちは UIKit teamの エンジニアのAdityaです コレクションビューなどのリスト形式は 多くのAppのデザインの基礎にあり 滑らかな動きが気持ちいいです リストやコレクションを 高速スクロールする方法を紹介し 旅行先候補の写真を閲覧できるAppを 作る仮定で説明していきたいと思います 一見ではシンプルそうですね 写真と文字がいくつかあります この動画ではセットアップの方法や 期待されるような十分な パフォーマンスを得る方法を説明します まずはdiffableデータソースや セル登録などのAPIを使った 強い基礎をつくる方法を伝授し 次にコレクションビューの ライフサイクルについて説明します 次にスクロールの引っかかりの原因と プリフェッチによる解決法 最後に非同期読込みのコンテンツを含む セルの更新法と デバイス共通の スクロールを提供する方法を PatrickがUI Image API にて説明します はい では Appが データを構成する過程を見ていきます 表示する投稿を検索しますが 投稿はそれぞれ 構造体の DestinationPostで表され 構造体はidenfiableに準拠 つまり識別子を保持する IDプロパティがあります 各DestinationPostに識別子があり 他のプロパティが変わっても不変です Diffableソースは オブジェクト自体でなく 識別子を保持するのです このAppではdiffableソース設定に IDプロパティを使い DestinationPostは使いません これがAppのdiffableソース DestinationPost.IDタイプで アイテム識別子を表します Sectionタイプは1ケースのENUM 1つのセクションしかないためです データソースの設定では 空のスナップショットを作り メインセクションに付加 バッキングストアから投稿をフェッチし それに識別子をアペンドします 識別子は不変なためこうすれば DestinationPostに 変更がある場合もdiffable上の表現は 変わらず保てます 最後にスナップショットを データソースに適用します iOS15以前はアニメーション無しの スナップショットは 内部でreloadDataに翻訳されましたが セルを全て除去し再構築する 必要が生じるため パフォーマンスに悪影響がありました 今後はアニメーション無しの スナップショットでも 差分を適用するだけで 余計な無駄はありません またdiffableソースも新しいメソッド reconfigureItemsを導入し 可視セルの更新が容易になりました 後で動作を説明します まずはソースからセルに データを読み込みましょう セル登録は各セルの設定を 一つの場所に集めdiffableソースの 識別子にも 手が届きやすくしてくれます UICollectionViewは セル登録の度に再利用キューを 準備するので一セルごとに 登録は一度限りにしましょう これは簡便化したセル登録です パスされたpostIDは DestinationPostと オブジェクト内の画像の入手に使い セルのタイトル画像を設定するときには DPのpropertyを使います 登録した後利用するには ソース内のセルプロバイダより 紫色 一番下のルーチンを呼び出します セルプロバイダを外部よりまず登録し それから内部で使用します プロバイダから登録すると コレクションビューが再利用できないので これはパフォーマンス上大事なポイントです セル設定ができるようになったら セル設定の順序ライフサイクルを 見ていきましょう セルの寿命には2つの段階があり それは準備段階とディスプレイ段階です 準備段階 最初のステップは セルをフェッチすること UICollectionViewは必要な度に ソースにセルを依頼します diffableソースの場合 セルプロバイダを走らせ結果を取得します 実行中登録情報をつかい 新セルをデキューするよう依頼 再利用プールにあるばあいは prepareForReuseをコールし セルをデキューします 再利用プールが空の場合は 新しいセルを設定します その後設定ハンドラーに セルを登録状態よりパス アイテム識別子と索引パスをもとに セル表示の準備をします セルは次の手順のためにビューに戻します
ビューはレイアウト設定をセルに問い セルのサイズを調整します これでセルの準備は完了 第二段階ディスプレイに映ります willDisplayCellを デリゲートにてコールしセルを可視化 スクリーンに映りました 映っている間はライフサイクルの 動きはありません 画面から外れると didEndDisplayingがコールされ 再利用プールに戻ります そこからは再度デキューも可能 繰り替えし表示ができます ではAppを使ってみましょう ペルーのクスコ カリブ海のセントルシアですね スクロールして他を見ようとしますが あまり滑らかな動きではありませんね
この スクロール中の休止はヒッチといいます ヒッチの原因を探るには まず画像切り替えの仕組みを見てみましょう フレームごとに タップなどの出来事が報告され それに応じビューやレイヤーが更新されます 例えば拡大中はスクロールビューの contentOffsetが 全てのビューの位置を動かします このような変化に応じ ビューやレイヤーはレイアウトを実行し この過程はコミットといいます レンダリングサーバーに レイヤーツリーを送信します フレームはコミットを その時点までに終わらせる コミット締切があり コミットの制限時間はAppの リフレッシュ速度により変わります 例えば120Hzという 高速設定のiPad Proだと 60HzのiPhoneに比べ 締切がきつくなります これはコレクションまたはテーブルビューの コミットの一例ですが 新しいセルが現れる時は セルの設定・レイアウト既存のセルが 画面を移動するだけのロスタイムがあり コミットはありません 後者の場合 新セルが不要で フレーム速度も速くなります やがて画面は新セルが映るまで スクロールされ 同パターンを繰り返します では 以前お見せしたヒッチは なぜ生じるのか コミットが長くかかり締切をすぎると 目的のフレームに更新が適応できなくなり 新しいフレームが出来るまで 以前のフレームを表示しますが これがヒッチの原因です スクロールの中断として反映されます
コミットその他のヒッチについては 「Explore UI animation hitches」 をご覧ください UICollectionViewと UITableViewha iOS15で新たなプリフェッチ方式を導入
スクロール中にヒッチが起こる 例にまた話を戻しますが ここで重要なのは セルが毎フレーム必要な訳ではないこと コミット時間が短く 仕事も少ない フレームがいくつかあります iOS 15のプリフェッチは余分な時間を活用し 短いコミットの間に次のセルを準備
セルが必要となれば 映し出すだけになります プリフェッチのセルが 速く処理できるのは前もって 読み込みをこなしていたからです ヒッチで画面が止まる時間を 空き時間に移すわけです フライングができたので ヒッチも回避できました コミット一つ一つに注目し 仕組みを詳しく見ていきましょう プリフェッチの前に 現フレームのコミットを実行 新たなセルはないので速く終わり 締め切りまで時間を残しています 次のフレームまで待つのではなく iOS15ではこの好機をとらえ 余分な時間で 次のセルをフェッチできます で 次が肝心なところです プリフェッチしたセルは重いため 通常よりも遅く コミットを始めます しかしそれにもかかわらず 締切のずっと前に コミットを完了します この例と以前の例を 比較してみましょう 締切を過ぎるコミットは一つもなく プリフェッチならヒッチは全くなくなります つまりヒッチを起こすことなく 通常の最大2倍の時間を創出できます さらに朗報なのはこの機能は iOS 155DKのAppなら すぐ使えること 実は最初の例は 14 SDKで作ったAppでした では15SDKでは どうなるのでしょか
すばらしいですね プリフェッチが予想通り働き スクロールが滑らかです コーディングも必要ありませんでした iOS 15SDKを使うのみ これを知っていただきたいです UICollectionViewについては iOS 10の同機能を増強し リストなどすべての構成に 対応可になりました なんとUITableviewでも プリフェッチが利用可能です プリフェッチは操作性を向上するだけでなく 電力消費を抑えバッテリー寿命を伸ばします セルが速く処理可能な場合この機能を使い ヒッチを避けながらエネルギーを 節約できるため ヒッチがもともとない場合も できるかぎり効率の良い レイアウト・セル処理を行いましょう セルのライフサイクルへの影響はどうでしょう これがプリフェッチなしのサイクル 二段階にはっきり分かれています プリフェッチの場合画面に読み込む前に 準備段階がなされています この機能を最大限に活用するには 準備段階で構成を終わらせるのです セルが映るのを待っていてはいけないのです プリフェッチングの一環として コレクションビュー変換時には サイズも調整がなされます プリフェッチ後は表示を前に待機する 中間状態が生じます この新段階については 配慮事項がいくつかあります スクロール方向が変わった時など 準備したセルが表示されない場合があります 既に表示されたセルは 画面から外れたらすぐ 準備状態に逆行し 同じ索引パスで二度以上の表示が可能です 表示が終われば再利用プールに直行の 前設定とは異なっています プリフェッチがヒッチを防ぐのは 余分な時間を活用しているからであり フレーム数が高いデバイスだと 依然スクロール中にヒッチが発生する可能性が ここでPatrickが引き継ぎ セルを構成する仕組み 画像表示の際コミットの 時間を減らす方法を 説明していきます では ハイレベル仕様部のPatrickです Appの例を使い 既存のセルを更新する方法 iOS15導入のAPIで 最高のパフォーマンスを 実現する方法をご説明します 今 Appはローカルファイルを使用 スクロールしセルを準備する時 画像はファイルシステムより 直ちに読み込まれます 次はリモートサーバー上の画像を 読み込みましょう セルが映った時 ビューの 画像を保持していない可能性もあります イメージビューが映った時は空になり サーバーリクエスト完了後やっと表示されます 新方式をサポートするため 登録情報の設定ハンドラーを 拡張してみましょう 登録画面の設定ハンドラーでは 既に記憶装置よりアセットをフェッチ済み 画像は引き渡されますが 完全なアセットではなく ダウンロードが必要な時も アセットオブジェクトはこれを isPlaceholderで指示し 正の場合は 画像をダウンロードするよう依頼します これが完了したらセルの イメージビューを更新できます ここでは既存のセルオブジェクトの イメージビューにアセットをセット しかしこれは間違いです セルは宛先ごとに再利用するため アセットの最後を読み込んだ時には 別の投稿がオブジェクトに 取り込まれているかもしれません セルを直接更新するのでなく コレクションビューのソースに 読み込みの必要性を伝えます iOS 15ではメソッド reconfigureItemsを導入し 準備済みのセルについてコールすると 登録画面の設定ハンドラーを取得します 新しいセルをデキュー・設定せず 既成のセルを再利用できるため reloadItemsと替えましょう 例のAppでは setPostNeedsUpdateとし IDについて新メソッドをコールします
次に 登録画面の設定ハンドラーに戻り 画像が未表示の場合 アセットをダウンロードし 新メソッドをコールします その後またハンドラーを呼び出しますが 今のところはfetchByIDが アセットを全て読み込みます これで全てのビュー更新コードを 一か所にまとめ非同時的にセルを 更新することができます downloadAssetを使い プリフェッチングのソース内で 準備時間を引き延ばすこともでき このデータのプリフェッチングは ダウンロードを始める時良策となります セルが映る前にダウンロードの時間を稼ぎ プレースホルダーが映る 時間を減らすことができるからです
Appにはどう反映されるのでしょうか 悪くはありませんが 明らかにヒッチが確認でき イメージの読み込みと 連動しているようです 新セルの準備ならヒッチングはなく あくまでも最高画質で ダウンロードするときヒッチがあります これは表示前にデコーディングが必要であり 非プレースホルダー等 特定のイメージは 解読に時間がかかるからです 登録画面の設定ハンドラーが コールされプレースホルダーがある場合 コードよりフルサイズ画像を 非同期要求し 設定を完了します ダウンロードが完了したら 最終イメージを使い設定ハンドラーを 再度コールします ビューが新しい画像をコミットするとき まずメインスレッドにて 画像を準備する必要があり これは時間がかかるため締切を過ぎ ヒッチが生じることがあります この準備過程は全ての画像が 表示前に終えなければなりません
また サーバーは bitmapというピクセル情報の 画像しか表示できません PNG HEIC JPEGなど 多様な形式になっており すべて圧縮しディスプレイのため 処理・アンパックをする必要があります 画像にコミットする時に メインスレッド上で ビューはこの処理を行います 理想は先んじて準備を済めせ 全て完了した状態で UIを更新すること そうすればメインスレッドを邪魔せず ヒッチも起こりません iOS 15では 画像準備APIを導入し 準備をいつ どこで行うか 裁量をデベロッパーに与えます このAPIはレンダラーが必要とする ピクセルデータのみの UIImageを生成するため イメージビューにしたら あとは処理もありません 同期式でどのスレッドでも 使えるAPIと 非同期式で内部的な シリアルキューでのみ使えるAPIがあり
UIImageのイメージビューにし プレースホルダーを設置したのち 新APIを呼び出すことで バックグラウンドで 画像を走る準備が完了です 完了すればイメージビューに切り替えます イメージの事前準備は画像中心の Appで重宝されますが 注意点もいくつかあります 準備されたイメージは 生のピクセルデータを含んでおり メモリに保持されていればいつでも ビューにてディスプレイができます ただ この場合メモリを多量消費するので 使う時を見極めましょう また このフォーマットは ディスク保存に適しません 原初のアセットをディスクに保存しましょう 最後のポイントは プリフェッチングの活用法 プリフェッチはイメージの ダウンロード・準備用の時間を創出し 処理に余裕を与えるため 長くプレースホルダーを映すことが なくなります 用例のAppでは すでに画像取得のため 非同期パスを設けていますが そこでダウンロードの終了後 完了ハンドラーを呼び出す前に アセットを準備する時間があります このアセットは大きく貴重なため 準備完了後はキャッシュしましょう イメージキャッシュは画像のサイズから メモリ使用量を推定し アセットが呼び出された際は サーバーを見る前に キャッシュを確認できます 小さなイメージなら 沢山キャッシュできます 大きな画像を扱うために iOS 15ではサムネイルを準備するという 似た系統のAPIを準備しており イメージを小さなサイズに 変えることができます 宛先の容量をふまえて 画像を読み取り処理することで CPUの時間やメモリが節約できます
画像準備APIと同様に UIimageのイメージビューに プレースホルダーをセット ここでサイズ変更APIを呼び出します ビューのサイズはサムネイルのサイズ
準備ができればビューのサイズを サムネイルに合わせます 画像準備APIとともに iOS15のAppにおいて ヒッチを解消する 新APIはこれを容易にしています イメージを処理する際は 画像が準備できればすぐUIを更新する 非同期APIを準備し 処理中は同期表示をしていても 安く 軽いプレースホルダーを 使用しましょう プリフェッチングreconfigureItemsにより 非同期アイテムのコレクション・リスト表示が かつてないほど簡単になっています
高速ビューを使い始めるには iOS 15 SDKでAppを製作し 新仕様を使えるようにしてください コレクションやテーブル表示で プリフェッチングを使う際 検査を忘れないでください ここで使用したAPIは全て イメージの準備やサムネイル化を Appにご使用の場合は ぜひご利用ください 豪速スピードのコレクション テーブルビューお楽しみを! ご視聴ありがとう [陽気な音楽]
-
-
1:25 - Structuring data
// Structuring data struct DestinationPost: Identifiable { // Each post has a unique identifier var id: String var title: String var numberOfLikes: Int var assetID: Asset.ID }
-
2:01 - Setting up diffable data source
// Setting up diffable data source class DestinationGridViewController: UIViewController { // Use DestinationPost.ID as the item identifier var dataSource: UICollectionViewDiffableDataSource<Section, DestinationPost.ID> private func setInitialData() { var snapshot = NSDiffableDataSourceSnapshot<Section, DestinationPost.ID>() // Only one section in this collection view, identified by Section.main snapshot.appendSections([.main]) // Get identifiers of all destination posts in our model and add to initial snapshot let itemIdentifiers = postStore.allPosts.map { $0.id } snapshot.appendItems(itemIdentifiers) dataSource.apply(snapshot, animatingDifferences: false) } }
-
3:47 - Creating cell registrations
// Cell registrations let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) cell.titleView.text = post.region cell.imageView.image = asset.image }
-
4:03 - Using cell registrations
// Cell registrations let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in ... } let dataSource = UICollectionViewDiffableDataSource<Section.ID, DestinationPost.ID>(collectionView: cv){ (collectionView, indexPath, postID) in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: postID) }
-
13:58 - Existing cell registration
// Existing cell registration let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) cell.titleView.text = post.region cell.imageView.image = asset.image }
-
14:17 - Updating cells asynchronously (wrong)
// Updating cells asynchronously let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) if asset.isPlaceholder { self.assetsStore.downloadAsset(post.assetID) { asset in cell.imageView.image = asset.image } } cell.titleView.text = post.region cell.imageView.image = asset.image }
-
15:15 - Reconfiguring items
private func setPostNeedsUpdate(id: DestinationPost.ID) { var snapshot = dataSource.snapshot() snapshot.reconfigureItems([id]) dataSource.apply(snapshot, animatingDifferences: true) }
-
15:23 - Updating cells asynchronously (correct)
// Updating cells asynchronously let cellRegistration = UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID> { (cell, indexPath, postID) in let post = self.postsStore.fetchByID(postID) let asset = self.assetsStore.fetchByID(post.assetID) if asset.isPlaceholder { self.assetsStore.downloadAsset(post.assetID) { _ in self.setPostNeedsUpdate(id: post.id) } } cell.titleView.text = post.region cell.imageView.image = asset.image }
-
15:52 - Data source prefetching
// Data source prefetching var prefetchingIndexPaths: [IndexPath: Cancellable] func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths [IndexPath]) { // Begin download work for indexPath in indexPaths { guard let post = fetchPost(at: indexPath) else { continue } prefetchingIndexPaths[indexPath] = assetsStore.loadAssetByID(post.assetID) } } func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { // Stop fetching for indexPath in indexPaths { prefetchingIndexPaths[indexPath]?.cancel() } }
-
18:43 - Using prepareForDisplay
// Using prepareForDisplay // Initialize the full image let fullImage = UIImage() // Set a placeholder before preparation imageView.image = placeholderImage // Prepare the full image fullImage.prepareForDisplay { preparedImage in DispatchQueue.main.async { self.imageView.image = preparedImage } }
-
19:51 - Asset downloading without image preparation
// Asset downloading – before image preparation func downloadAsset(_ id: Asset.ID, completionHandler: @escaping (Asset) -> Void) -> Cancellable { return fetchAssetFromServer(assetID: id) { asset in DispatchQueue.main.async { completionHandler(asset) } } }
-
19:58 - Asset downloading with image preparation
// Asset downloading – with image preparation func downloadAsset(_ id: Asset.ID, completionHandler: @escaping (Asset) -> Void) -> Cancellable { // Check for an already prepared image if let preparedAsset = imageCache.fetchByID(id) { completionHandler(preparedAsset) return AnyCancellable {} } return fetchAssetFromServer(assetID: id) { asset in asset.image.prepareForDisplay { preparedImage in // Store the image in the cache. self.imageCache.add(asset: asset.withImage(preparedImage!)) DispatchQueue.main.async { completionHandler(asset) } } } }
-
20:50 - Using prepareThumbnail
// Using prepareThumbnail // Initialize the full image let profileImage = UIImage(...) // Set a placeholder before preparation posterAvatarView.image = placeholderImage // Prepare the image profileImage.prepareThumbnail(of: posterAvatarView.bounds.size) { thumbnailImage in DispatchQueue.main.async { self.posterAvatarView.image = thumbnailImage } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。