Prefetching Data in UITableView and UICollectionView
Prefetching in UITableView
and UICollectionView
is a UIKit feature introduced in iOS 10 that allows developers to preload data for cells before they appear on screen, improving scrolling performance and reducing perceived latency. By anticipating which cells will be displayed, you can fetch data (e.g., network requests, images) in advance. This document provides an in-depth explanation of prefetching, including the UITableViewDataSourcePrefetching
and UICollectionViewDataSourcePrefetching
protocols, their methods, examples, and best practices for both views.
Overview of Prefetching
Prefetching optimizes data loading by initiating tasks for cells that are about to become visible during scrolling. It works alongside cell reuse to ensure smooth scrolling, especially for data-heavy apps (e.g., social media feeds, image galleries). The system calls prefetch methods based on the user’s scroll direction and speed, allowing you to prepare data early.
Key Protocols
UITableViewDataSourcePrefetching
: For prefetching data inUITableView
.UICollectionViewDataSourcePrefetching
: For prefetching data inUICollectionView
.
Key Methods
Protocol Method | Description |
---|---|
tableView(_:prefetchRowsAt:) | Called to prefetch data for rows at specified index paths in UITableView . |
tableView(_:cancelPrefetchingForRowsAt:) | Called to cancel prefetching for rows no longer needed. |
collectionView(_:prefetchItemsAt:) | Called to prefetch data for items at specified index paths in UICollectionView . |
collectionView(_:cancelPrefetchingForItemsAt:) | Called to cancel prefetching for items no longer needed. |
When to Use Prefetching
- Fetching network data (e.g., API responses, images).
- Performing expensive computations or disk I/O.
- Loading large datasets incrementally.
When Not to Use Prefetching
- For lightweight data already in memory.
- If prefetching causes excessive resource usage (e.g., too many network requests).
Prefetching in UITableView
UITableView
uses the UITableViewDataSourcePrefetching
protocol to prefetch data for rows. You adopt this protocol in your data source object and set it as the table view’s prefetchDataSource
.
Example: Prefetching Images in UITableView
This example demonstrates prefetching images from a network for a table view displaying a list of items with thumbnails.
import UIKit
import SnapKit
struct Item {
let title: String
let imageURL: URL
}
class CustomTableViewCell: UITableViewCell {
static let identifier = "CustomCell"
let titleLabel = UILabel()
let thumbnailImageView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
thumbnailImageView.contentMode = .scaleAspectFill
thumbnailImageView.clipsToBounds = true
let stackView = UIStackView(arrangedSubviews: [thumbnailImageView, titleLabel])
stackView.axis = .horizontal
stackView.spacing = 8
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalTo(contentView).inset(8)
}
thumbnailImageView.snp.makeConstraints { make in
make.size.equalTo(50)
}
}
func configure(with item: Item, image: UIImage?) {
titleLabel.text = item.title
thumbnailImageView.image = image
}
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDataSourcePrefetching {
private let tableView = UITableView()
private var items: [Item] = (1...100).map {
Item(title: "Item \($0)", imageURL: URL(string: "https://example.com/image\($0).jpg")!)
}
private var imageCache: [URL: UIImage] = [:]
private var activeTasks: [URL: URLSessionDataTask] = [:]
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
private func setupTableView() {
tableView.dataSource = self
tableView.prefetchDataSource = self
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
tableView.rowHeight = 66
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
let item = items[indexPath.row]
let image = imageCache[item.imageURL]
cell.configure(with: item, image: image)
if image == nil && activeTasks[item.imageURL] == nil {
fetchImage(for: item.imageURL) { image in
self.imageCache[item.imageURL] = image
DispatchQueue.main.async {
if let visibleCell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell {
visibleCell.configure(with: item, image: image)
}
}
}
}
return cell
}
// MARK: - UITableViewDataSourcePrefetching
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.row]
guard imageCache[item.imageURL] == nil, activeTasks[item.imageURL] == nil else { continue }
fetchImage(for: item.imageURL) { image in
self.imageCache[item.imageURL] = image
self.activeTasks[item.imageURL] = nil
}
}
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.row]
if let task = activeTasks[item.imageURL] {
task.cancel()
activeTasks[item.imageURL] = nil
}
}
}
private func fetchImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data, let image = UIImage(data: data) else {
completion(nil)
return
}
completion(image)
}
activeTasks[url] = task
task.resume()
}
}
Explanation
- Setup: The table view is configured with a custom cell displaying a title and thumbnail.
- Data Source:
cellForRowAt
checks the cache for images and initiates a fetch if needed. - Prefetching:
prefetchRowsAt
starts image downloads for upcoming rows, storing tasks to track them. - Cancellation:
cancelPrefetchingForRowsAt
cancels tasks for rows no longer needed, preventing unnecessary network usage. - Image Cache: A dictionary stores loaded images to avoid redundant fetches.
Prefetching in UICollectionView
UICollectionView
uses the UICollectionViewDataSourcePrefetching
protocol to prefetch data for items. Similar to UITableView
, you adopt this protocol and set the collection view’s prefetchDataSource
.
Example: Prefetching Images in UICollectionView
This example demonstrates prefetching images for a collection view with a grid layout.
import UIKit
import SnapKit
struct Item {
let title: String
let imageURL: URL
}
class CustomCollectionViewCell: UICollectionViewCell {
static let identifier = "CustomCell"
let titleLabel = UILabel()
let thumbnailImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textAlignment = .center
thumbnailImageView.contentMode = .scaleAspectFill
thumbnailImageView.clipsToBounds = true
thumbnailImageView.backgroundColor = .systemGray6
let stackView = UIStackView(arrangedSubviews: [thumbnailImageView, titleLabel])
stackView.axis = .vertical
stackView.spacing = 4
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalTo(contentView).inset(4)
}
thumbnailImageView.snp.makeConstraints { make in
make.height.equalTo(thumbnailImageView.snp.width)
}
}
func configure(with item: Item, image: UIImage?) {
titleLabel.text = item.title
thumbnailImageView.image = image
}
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDataSourcePrefetching {
private let collectionView: UICollectionView
private var items: [Item] = (1...100).map {
Item(title: "Item \($0)", imageURL: URL(string: "https://example.com/image\($0).jpg")!)
}
private var imageCache: [URL: UIImage] = [:]
private var activeTasks: [URL: URLSessionDataTask] = [:]
init() {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
collectionView.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.prefetchDataSource = self
collectionView.delegate = self
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
let item = items[indexPath.item]
let image = imageCache[item.imageURL]
cell.configure(with: item, image: image)
if image == nil && activeTasks[item.imageURL] == nil {
fetchImage(for: item.imageURL) { image in
self.imageCache[item.imageURL] = image
DispatchQueue.main.async {
if let visibleCell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewCell {
visibleCell.configure(with: item, image: image)
}
}
}
}
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.frame.width - 30) / 2 // 2 items per row
return CGSize(width: width, height: width + 30) // Account for title
}
// MARK: - UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.item]
guard imageCache[item.imageURL] == nil, activeTasks[item.imageURL] == nil else { continue }
fetchImage(for: item.imageURL) { image in
self.imageCache[item.imageURL] = image
self.activeTasks[item.imageURL] = nil
}
}
}
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.item]
if let task = activeTasks[item.imageURL] {
task.cancel()
activeTasks[item.imageURL] = nil
}
}
}
private func fetchImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data, let image = UIImage(data: data) else {
completion(nil)
return
}
completion(image)
}
activeTasks[url] = task
task.resume()
}
}
Explanation
- Setup: A collection view with a grid layout displays items with thumbnails and titles.
- Data Source:
cellForItemAt
configures cells, checking the cache and fetching images if needed. - Prefetching:
prefetchItemsAt
initiates image downloads for upcoming items. - Cancellation:
cancelPrefetchingForItemsAt
cancels tasks for items no longer needed. - Layout:
UICollectionViewFlowLayout
ensures a two-column grid with dynamic sizing.
Best Practices for Prefetching
- Use Caching: Store fetched data (e.g., images) in memory or disk to avoid redundant requests.
- Implement Cancellation: Always handle
cancelPrefetching
to free resources and prevent unnecessary work. - Throttle Requests: Limit concurrent network requests using a queue or operation management (e.g.,
OperationQueue
). - Prioritize Visible Cells: Ensure
cellForRowAt
orcellForItemAt
can fetch data independently if prefetching hasn’t completed. - Test Performance: Profile with Instruments to ensure prefetching doesn’t overload the system.
- Combine with Diffable Data Sources: Use
UITableViewDiffableDataSource
orUICollectionViewDiffableDataSource
for modern apps to manage data updates efficiently. - Handle Errors: Account for network failures or invalid data in prefetching logic.
- Accessibility: Ensure prefetching doesn’t interfere with VoiceOver or dynamic type.
Example: Throttling with OperationQueue
To limit concurrent image downloads, use an OperationQueue
:
private let imageQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4 // Limit concurrent downloads
return queue
}()
private func fetchImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
let operation = BlockOperation {
guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
completion(nil)
return
}
completion(image)
}
activeTasks[url] = operation
imageQueue.addOperation(operation)
}
Troubleshooting
- Prefetching Not Triggered: Ensure
prefetchDataSource
is set and index paths are valid. - Excessive Network Usage: Implement cancellation and limit concurrent tasks.
- Stale Data: Clear cache or update data when refreshing the table/collection view.
- Performance Issues: Profile with Instruments to identify bottlenecks in prefetching logic.
- Cells Not Updating: Verify completion handlers update the UI on the main thread.
- Cancellation Not Working: Check that tasks are properly tracked and canceled.
Example: Complete Setup with Diffable Data Source and Prefetching
This example combines prefetching with UICollectionViewDiffableDataSource
for a modern implementation.
import UIKit
import SnapKit
struct Item: Hashable {
let id = UUID()
let title: String
let imageURL: URL
}
class CustomCollectionViewCell: UICollectionViewCell {
static let identifier = "CustomCell"
let titleLabel = UILabel()
let thumbnailImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textAlignment = .center
thumbnailImageView.contentMode = .scaleAspectFill
thumbnailImageView.clipsToBounds = true
thumbnailImageView.backgroundColor = .systemGray6
let stackView = UIStackView(arrangedSubviews: [thumbnailImageView, titleLabel])
stackView.axis = .vertical
stackView.spacing = 4
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalTo(contentView).inset(4)
}
thumbnailImageView.snp.makeConstraints { make in
make.height.equalTo(thumbnailImageView.snp.width)
}
}
func configure(with item: Item, image: UIImage?) {
titleLabel.text = item.title
thumbnailImageView.image = image
accessibilityLabel = item.title
}
}
enum Section { case main }
typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSourcePrefetching {
private let collectionView: UICollectionView
private var dataSource: DataSource!
private var items: [Item] = (1...100).map {
Item(title: "Item \($0)", imageURL: URL(string: "https://example.com/image\($0).jpg")!)
}
private var imageCache: [URL: UIImage] = [:]
private var activeTasks: [URL: URLSessionDataTask] = [:]
private let imageQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
return queue
}()
init() {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
configureDataSource()
applySnapshot()
}
private func setupCollectionView() {
collectionView.backgroundColor = .systemBackground
collectionView.prefetchDataSource = self
collectionView.delegate = self
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
private func configureDataSource() {
dataSource = DataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
let image = self?.imageCache[item.imageURL]
cell.configure(with: item, image: image)
if image == nil && self?.activeTasks[item.imageURL] == nil {
self?.fetchImage(for: item.imageURL) { image in
self?.imageCache[item.imageURL] = image
DispatchQueue.main.async {
if let visibleCell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewCell {
visibleCell.configure(with: item, image: image)
}
}
}
}
return cell
}
}
private func applySnapshot(animatingDifferences: Bool = false) {
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.frame.width - 30) / 2
return CGSize(width: width, height: width + 30)
}
// MARK: - UICollectionViewDataSourcePrefetching
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.item]
guard imageCache[item.imageURL] == nil, activeTasks[item.imageURL] == nil else { continue }
fetchImage(for: item.imageURL) { image in
self.imageCache[item.imageURL] = image
self.activeTasks[item.imageURL] = nil
}
}
}
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let item = items[indexPath.item]
if let task = activeTasks[item.imageURL] {
task.cancel()
activeTasks[item.imageURL] = nil
}
}
}
private func fetchImage(for url: URL, completion: @escaping (UIImage?) -> Void) {
let operation = BlockOperation {
guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
completion(nil)
return
}
completion(image)
}
activeTasks[url] = operation
imageQueue.addOperation(operation)
}
}
// App setup
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let navController = UINavigationController(rootViewController: ViewController())
window?.rootViewController = navController
window?.makeKeyAndVisible()
return true
}
}
Explanation
- Diffable Data Source: Manages data updates with animated transitions.
- Prefetching: Preloads images for upcoming items, with cancellation support.
- Operation Queue: Limits concurrent downloads to 4 for better resource management.
- Dynamic Layout: Uses
UICollectionViewFlowLayout
for a responsive grid.