WWDC Quick Look 💓 By SwiftGGTeam
Make blazing fast lists and collection views

Make blazing fast lists and collection views

元の動画を見る

ハイライト

Apple は、iOS 15 のセルのプリフェッチ、差分可能なデータ ソースの更新、および UITableView および UICollectionView の UIImage 準備デコード API を改善しました。開発者は、スクロール フレームのドロップ、非同期コンテンツの位置ずれ、大規模な画像デコードのフリーズを削減できます。

主要内容

多くのアプリケーションのホームページはリストです。旅行アプリでは目的地の写真が表示され、ソーシャル アプリでは投稿が表示され、e コマース アプリでは商品カードが表示されます。各セルは単純に見えます。画像と 2 行のテキストです。ユーザーがスワイプするとすぐに問題が明らかになります。

古いアプローチでは、セルが表示される直前にフレームに多大な労力がかかります。コレクション ビューは、セルを取得し、コンテンツを構成し、レイアウトを計算し、結果をレンダリング システムに送信する必要があります。この送信が現在の画面リフレッシュ レートで指定された時間枠を超えると、ユーザーには一時停止が表示されます。

iOS 15 では、まずシステムレベルのプリフェッチが変更されました。 UITableView と UICollectionView は、スクロール中のアイドル時間を利用して、次のセルを事前に準備します。画面に表示されると、システムはすでに準備されている結果を表示するだけで済みます。

この最適化では、ビジネス コードを変更する必要はありません。 iOS 15 SDK を使用してビルドしている限り、システムは新しいセルのプリフェッチ メカニズムを有効にすることができます。 UICollectionView のリスト、その他の構成レイアウト、および UITableView についても説明します。

システムのプリフェッチは時間を稼ぐだけです。セル構成が非常に遅い場合、またはイメージがメインスレッドでデコードされる場合でも、フレームはドロップされます。したがって、Apple は 3 つのサポート ルールを提供しています。データ ソースは安定した ID を保存し、非同期コンテンツはreconfigureItems更新して、最初に写真を使用してくださいprepareForDisplayまたはprepareThumbnail準備をしましょう。

安定した ID により、diffable データ ソースの作業が軽減されます

Diffable データ ソースにはモデル識別子が格納されます。モデルオブジェクト全体を保存するべきではありません。このようにして、「いいね!」の数、タイトル、写真のステータスが変化しても、アイテムのアイデンティティは安定したままになります。

安定した ID の利点は簡単です。スナップショットが変更されると、システムは実際に変更された項目のみを処理できます。 iOS 15 では、アニメーションなしの適用の動作も改善されています。以前は、次のことが可能でした。reloadData、画面上のすべてのセルの破棄と再構築を避けるために、差分が適用されるようになりました。

セル登録は一度作成する必要があります

セル登録 (セル登録) は、特定のタイプのセルの構成ロジックを一元管理します。 diffable データ ソースからアイテム ID を受け取り、ストアからモデルと画像を取得します。

ここには簡単な落とし穴があります。登録は携帯電話プロバイダーの外部で作成する必要があるということです。プロバイダーが呼び出されるたびに新しい登録が作成される場合、コレクション ビューは元のセルを再利用できません。

非同期画像はセルを直接キャプチャできません

リモート画像の読み込みが完了すると、元のセルが別の投稿に再利用されている可能性があります。コールバックに直接記述するcell.imageView.image = ...、画像を間違った場所に配置します。

iOS15reconfigureItemsこの問題を解決してください。イメージがダウンロードされた後、どの ID を再構成する必要があるかがデータ ソースにのみ通知されます。システムは登録ハンドラーを再実行し、既存のセルを再利用します。

大きな画像は表示可能なビットマップとして事前に準備する必要があります

通常、画像ファイルは JPEG、PNG、または HEIC です。UIImageView新しい画像を画面に送信するときは、圧縮形式をピクセル データにデコードする必要があります。このプロセスはメインスレッドで発生する可能性があり、時間が経つと提出期限に間に合わなくなる可能性があります。

iOS 15 の UIImage 準備 API を使用すると、これを事前に行うことができます。prepareForDisplay生成準備完了UIImageprepareThumbnailまた、画像をターゲット サイズに縮小して、CPU とメモリを節約します。

詳細

(01:25) リストデータを安定した ID で整理する

Apple のサンプル アプリでは、DestinationPost旅行先の投稿を表します。このタイプは従うIdentifiable、コアフィールドは安定していますid

struct DestinationPost: Identifiable {
    // Each post has a unique identifier
    var id: String

    var title: String
    var numberOfLikes: Int
    var assetID: Asset.ID
}

キーポイント:

  • DestinationPost: Identifiable各投稿が識別可能であることを示します。
  • id各投稿の一意の識別子であり、他のプロパティが変更されても変更されません。
  • titleそしてnumberOfLikesこれは変化するコンテンツであり、diffable データ ソースのアイテム ID として使用するのには適していません。
  • assetID投稿を画像リソースに関連付けてから、セルの構成中にリソース ストアに移動して画像を取得します。

次に、データ ソースは次を使用します。DestinationPost.IDアイテム識別子として。

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)
    }
}

キーポイント:

  • UICollectionViewDiffableDataSource<Section, DestinationPost.ID>アイテムの種類を投稿 ID に限定します。
  • NSDiffableDataSourceSnapshot<Section, DestinationPost.ID>()同じ ID を使用してスナップショットを作成します。
  • snapshot.appendSections([.main])独自のセクションを追加します。
  • postStore.allPosts.map { $0.id }モデル配列から安定した識別子を抽出します。
  • snapshot.appendItems(itemIdentifiers)ID をスナップショットに入力するだけです。
  • dataSource.apply(snapshot, animatingDifferences: false)初期データを適用します。 iOS 15 以降では、アニメーションを使用せずに適用すると差分が適用され、余分な再構築作業が軽減されます。

(03:47) セル登録を使用してセルを一元的に構成する

Cell登録のハンドラーは受信します。cellindexPathそしてpostID。例で使用されるpostID投稿を見つけて再度使用しますassetID画像リソースを確認してください。

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
}

キーポイント:

  • UICollectionView.CellRegistration<DestinationPostCell, DestinationPost.ID>セルの種類と項目IDの種類を宣言します。
  • ハンドラーpostIDdiffable データソースから。
  • postsStore.fetchByID(postID)ID を使用して完全な投稿データを取得します。
  • assetsStore.fetchByID(post.assetID)リソース ID を使用して画像リソースを取得します。
  • cell.titleView.text = post.regionタイトルの内容を設定します。
  • cell.imageView.image = asset.image現在利用可能な画像を設定します。これはプレースホルダー画像である可能性があります。

登録を使用する場合は、に渡しますdequeueConfiguredReusableCell

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)
}

キーポイント:

  • cellRegistrationデータ ソースのセル プロバイダーの外部で作成されます。
  • UICollectionViewDiffableDataSourceセルプロバイダーはセルを返却する責任があります。
  • dequeueConfiguredReusableCell再利用可能なセルが取り出され、登録ハンドラーが実行されます。
  • item: postID現在のアイテム ID をハンドラーに渡します。
  • プロバイダー内で登録が繰り返し作成されると、コレクション ビューは登録ごとに新しい再利用キューを維持することになり、再利用の効果が悪化します。

(08:22) システムにアイドル フレームを使用して事前にセルを準備させます

このセクションには新しいビジネス API はありません。主要な変更は UIKit 内にあります。iOS 15 の UITableView と UICollectionView は、短いコミットの後に残りの時間を使用してセルをプリフェッチします。

// Build with the iOS 15 SDK to enable the new cell prefetching behavior
// in UITableView and UICollectionView.

キーポイント:

  • セルのライフサイクルは、準備と表示に分かれています。
  • 準備には、セルの取得、セル プロバイダーの実行、登録ハンドラーの実行、レイアウト プロパティのクエリ、サイズの計算が含まれます。
  • iOS 15 では、セルが画面に表示される前に準備が進みます。
  • たとえば、ユーザーが突然逆方向にスクロールした場合、準備されたセルが表示されない場合があります。
  • 準備された同じセルが画面外に出た後に待機状態になり、再び同じインデックス パスに表示される場合があります。
  • 重労働は準備段階で完了する必要があります。待たずに完了してください。willDisplayもう一度やってください。

(14:17) 非同期コールバックで古いセルを直接更新しないでください。

リモート イメージのシナリオでは、リソース ストアは最初にプレースホルダー イメージを返します。間違った書き方は、ダウンロード コールバック内で直接変更することです。cell

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
}

キーポイント:

  • asset.isPlaceholder現在のリソースがまだプレースホルダー画像であることを示します。
  • downloadAsset非同期ダウンロードを開始します。
  • キャプチャされたコールバックcell物体。
  • セルは再利用されます。ダウンロードが完了するまでに、オブジェクトはすでに別の投稿を表している可能性があります。
  • このコードを使用すると、画像の位置がずれることがあります。

(15:15) reconfigureItems を使用して構成ロジックを再実行します

正しいアプローチは、更新リクエストをデータ ソースに返すことです。 iOS15reconfigureItems新しいセルの再デキューを避けるために、既存のセルが再構成されます。

private func setPostNeedsUpdate(id: DestinationPost.ID) {
    var snapshot = dataSource.snapshot()
    snapshot.reconfigureItems([id])
    dataSource.apply(snapshot, animatingDifferences: true)
}

キーポイント:

  • dataSource.snapshot()現在のスナップショットを取り出します。
  • snapshot.reconfigureItems([id])指定された項目を再構成が必要なものとしてマークします。
  • dataSource.applyこの構成更新を適用します。
  • reconfigureItems項目の既存のセルが再利用され、登録ハンドラーが再実行されます。
  • コンテンツは変更されるが、アイテムのアイデンティティは変更されないシナリオに適しています。

ダウンロードが完了したら、電話するだけですsetPostNeedsUpdate

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
}

キーポイント:

  • ハンドラーは毎回最初に押されますpostID現在の投稿を取得します。
  • プレースホルダー画像を取得したら、完全なリソースのダウンロードを開始します。
  • ダウンロード コールバックは古いセルを保持したり変更したりしなくなりました。
  • setPostNeedsUpdate(id: post.id)この投稿を再構成するようデータ ソースにのみ通知してください。
  • ハンドラーを2回目に実行するとき、fetchByID完全な画像リソースが返されます。
  • ビュー更新ロジックは依然として登録ハンドラーに集中しています。

(15:52) データ ソースのプリフェッチを使用してコンテンツを事前にダウンロードする

システムは事前にセルを準備します。データを事前に準備することもできます。UICollectionViewDataSourcePrefetchingネットワーク要求を開始し、プレースホルダー画像の表示時間を短縮するのに適しています。

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()
    }
}

キーポイント:

  • prefetchingIndexPathsインデックスパスとダウンロードタスクの対応関係を保存します。
  • prefetchItemsAtセルを表示する必要がある前に呼び出されます。
  • fetchPost(at:)インデックス パスに基づいて投稿を検索します。
  • assetsStore.loadAssetByID(post.assetID)リソースのロードを開始します。
  • cancelPrefetchingForItemsAtプリフェッチが必要なくなったときに呼び出されます。
  • cancel()ネットワークとバッテリーの無駄を避けるために、対応するダウンロードを停止します。

(18:43) prepareForDisplay を使用して大きな画像を事前に準備します

画像は表示する前にビットマップにデコードする必要があります。 iOS15prepareForDisplayこの手順を事前に完了してから、メインスレッドに戻って画像ビューを更新できます。

// 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
    }
}

キーポイント:

  • fullImageそれが本来の全体像です。
  • imageView.image = placeholderImage最初に安価なプレースホルダーを表示します。
  • prepareForDisplay背景に表示するのに適した画像を用意します。
  • コールバック内preparedImage準備された UIImage です。
  • DispatchQueue.main.asyncメインスレッドに戻って UI を更新します。
  • 準備されたイメージを設定する場合、イメージ ビューはコストのかかる準備作業を行う必要がなくなりました。

ダウンロード パスでは、完了ハンドラーの前に準備とキャッシュを置くことができます。

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)
            }
        }
    }
}

キーポイント:

  • imageCache.fetchByID(id)まずは用意されている画像リソースを探します。
  • キャッシュを見つけたらすぐに実行completionHandler
  • AnyCancellable {}メソッドのシグネチャの一貫性を保ちながら、キャンセル可能なオブジェクトを返します。
  • fetchAssetFromServerオリジナルのソースをダウンロードします。
  • asset.image.prepareForDisplayダウンロード後に画像を準備します。
  • imageCache.add(asset: asset.withImage(preparedImage!))準備したイメージをメモリ キャッシュに置きます。
  • DispatchQueue.main.async完了ハンドラーがメインスレッドでコールバックされていることを確認してください。

(20:50) prepareThumbnail を使用して、ターゲット サイズのサムネイルを準備します

アバターと小さな写真はフルサイズの画像である必要はありません。prepareThumbnail(of:)画像はターゲット サイズに合わせて拡大縮小されて準備され、CPU とメモリの使用量が削減されます。

// 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
    }
}

キーポイント:

  • profileImage元の絵です。
  • posterAvatarView.image = placeholderImage最初にプレースホルダー画像を表示します。
  • posterAvatarView.bounds.sizeターゲットの表示サイズを指定します。
  • prepareThumbnail(of:)目的のサイズのサムネイルを用意します。
  • コールバックの取得thumbnailImage
  • メインスレッドはサムネイルをアバタービューに設定します。

重要ポイント

  1. やるべきこと: 安定した ID データ レイヤーを画像ウォーターフォールに追加します。 実行する価値がある理由: diffable データ ソースは安定した識別子に依存します。内容が変更された場合は、対応する項目のみを再設定できます。 開始方法: モデルを準拠させるIdentifiable、スナップショットは保存のみModel.ID, 次にセル登録のIDを使ってストアを確認します。

  2. 何をすべきか: リモート画像リストをプレースホルダー画像に変更します +reconfigureItems実行する価値がある理由: セルを直接更新するダウンロード コールバックは位置ずれを起こしやすいです。reconfigureItems構成ロジックは項目 ID によって再実行されます。 開始方法: リソース ストアは、ダウンロードの完了後に呼び出されるプレースホルダー アセットを返します。snapshot.reconfigureItems([id])

  3. やるべきこと: ホームページ上の大きな画像用にメモリレベルで準備された画像キャッシュを作成します。 実行する価値がある理由:prepareForDisplay画像表示に必要なビットマップの準備を事前に完了できるため、メインスレッドへの送信プレッシャーが軽減されます。 開始方法: ダウンロード完了後に呼び出されますasset.image.prepareForDisplay、バンドルasset.withImage(preparedImage)キャッシュに入れます。

  4. やるべきこと: アバター、製品のサムネイル、フォト アルバム グリッドにはターゲット サイズのサムネイルを使用します。 実行する価値がある理由:prepareThumbnail(of:)画像はビュー サイズに応じて処理されるため、CPU 時間とメモリが節約されます。 開始方法: 画像ビューを使用するbounds.size目標サイズとして、準備が完了したらメインスレッドに戻って画像を設定します。

  5. 対処方法: リスト ネットワーク リソースのデータ ソース プリフェッチにアクセスします。 実行する価値がある理由: プリフェッチを使用すると、画像のダウンロードと準備をより早く開始できるため、ユーザーがプレースホルダー画像を表示する時間が短縮されます。 開始方法: 実装collectionView(_:prefetchItemsAt:)ダウンロードを開始して、cancelPrefetchingForItemsAt無駄なタスクをキャンセルします。

関連セッション

  • UIKit の新機能 — iOS 15 UIKit の API、パフォーマンス、インターフェイスの更新について説明します。
  • iPad アプリを次のレベルに — 複雑なリスト アプリに適した、iPad アプリのウィンドウ、キーボード、ポインターの操作を学習します。
  • Swift で構造化された同時実行性を探る — Swift の同時実行性を使用して、リスト内の非同期ダウンロードおよびキャンセル ロジックを変換します。
  • Showcase app data in Spotlight — ローカル データを Spotlight にインデックス付けして、リスト コンテンツの検索エントリを補完します。

コメント

GitHub Issues · utterances