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 ↗Code shown on screen · 13 snippets
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
} 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)
}
} 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
} 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)
} 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
} 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
} Reconfiguring items
private func setPostNeedsUpdate(id: DestinationPost.ID) {
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems([id])
dataSource.apply(snapshot, animatingDifferences: true)
} 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
} 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()
}
} 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
}
} 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)
}
}
} 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)
}
}
}
} 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
}
} Resources
Related sessions
-
27 min -
12 min -
10 min