Skip to content

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 in UITableView.
  • UICollectionViewDataSourcePrefetching: For prefetching data in UICollectionView.

Key Methods

Protocol MethodDescription
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.

swift
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.

swift
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 or cellForItemAt 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 or UICollectionViewDiffableDataSource 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:

swift
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.

swift
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.

Resources

Released under the MIT License.