Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2021 SwiftUI & UI Frameworks

WWDC21 · 23 min · SwiftUI & UI Frameworks

Make blazing fast lists and collection views

Build consistently smooth scrolling list and collection views: Explore the lifecycle of a cell and learn how to apply that knowledge to eliminate rough scrolling and missed frames. We’ll also show you how to improve your overall scrolling experience and avoid costly hitches, with optimized image loading and automatic cell prefetching. To get the most out of this video, we recommend a basic familiarity with diffable data sources and compositional layout.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 13 snippets

Structuring data swift · at 1:25 ↗
// Structuring data

struct DestinationPost: Identifiable {
    // Each post has a unique identifier
    var id: String
    
    var title: String
    var numberOfLikes: Int
    var assetID: Asset.ID
}
Setting up diffable data source swift · at 2:01 ↗
// 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)
    }
}
Creating cell registrations swift · at 3:47 ↗
// 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
}
Using cell registrations swift · at 4:03 ↗
// 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)
}
Existing cell registration swift · at 13:58 ↗
// 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
}
Updating cells asynchronously (wrong) swift · at 14:17 ↗
// 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
}
Reconfiguring items swift · at 15:15 ↗
private func setPostNeedsUpdate(id: DestinationPost.ID) {
    var snapshot = dataSource.snapshot()
    snapshot.reconfigureItems([id])
    dataSource.apply(snapshot, animatingDifferences: true)
}
Updating cells asynchronously (correct) swift · at 15:23 ↗
// 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
}
Data source prefetching swift · at 15:52 ↗
// 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()
    }
}
Using prepareForDisplay swift · at 18:43 ↗
// 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
    }
}
Asset downloading without image preparation swift · at 19:51 ↗
// 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)
        }
    }
}
Asset downloading with image preparation swift · at 19:58 ↗
// 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)
            }
        }
    }
}
Using prepareThumbnail swift · at 20:50 ↗
// 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
    }
}

Resources