11

I have a very simple UICollectionView that uses compositional layout to easily achieve dynamic cell heights. Unfortunately doing that seems to disable content prefetching using UICollectionViewDataSourcePrefetching. In the following sample code, the collectionView(_:prefetchItemsAt:) method is called only once, upon initial display of the collection view. No scrolling action leads to further calls to the method.

What can I do to get prefetching working?

class ViewController: UIViewController, 
    UICollectionViewDataSource,
    UICollectionViewDelegate,
    UICollectionViewDataSourcePrefetching
{
    @IBOutlet var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.collectionViewLayout = createLayout()
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.prefetchDataSource = self
        collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell")
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 100
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
        cell.label.text = String(repeating: "\(indexPath) ", count: indexPath.item)

        return cell
    }

    // this is only called once. Why?
    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        print("prefetch for \(indexPaths)")
    }

    private func createLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (_, _) -> NSCollectionLayoutSection? in

            let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .estimated(44))
            let item = NSCollectionLayoutItem(layoutSize: size)

            let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)
            group.interItemSpacing = .fixed(16)

            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
            section.interGroupSpacing = 8

            return section
        }

        return layout
    }
}

class MyCell: UICollectionViewCell {
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

(Using Xcode 11.5 / iOS 13.5. The collectionView outlet is connected to a full-screen instance in a storyboard)

EDIT: Further testing shows that heightDimension: .estimated(44) seems to be the cause. Replacing this with .absolute(44) makes prefetching work again, but that of course defeats the purpose of having such a layout with multi-line text. Seems like a bug, and I've filed FB7849272 if anyone would like to dupe it.

EDIT/2: For the time being, I can avoid this situation by using a plain old flow layout and calculate each individual cell height, this also makes prefetching work again. Nevertheless, I'm curious if there isn't a workaround while still using compositional layouts, so I added a bounty.

5
  • 2
    I have the same problem...
    – jlandyr
    Commented Jul 15, 2020 at 20:52
  • Have you managed to make it work?
    – zrfrank
    Commented Nov 16, 2020 at 9:14
  • 2
    No, the issue was unfortunately still present in iOS 14 when I last tested it.
    – Gereon
    Commented Nov 16, 2020 at 12:43
  • 2
    This is still an issue on iOS 14.5…
    – Vin Gazoil
    Commented Mar 7, 2021 at 14:59
  • Fixed in Xcode 13.1, see below.
    – Gereon
    Commented Nov 19, 2021 at 12:38

4 Answers 4

0

Yes..!! there is not a way to use prefetching with dynamic sized cells you have to use collectionView willDisplay forItemAt for it

0

I've just received feedback from Apple asking me to re-test this issue in Xcode 13.1, and lo and behold, the bug has indeed been fixed.

1
  • Still happened on Xcode 14.3.1
    – Abdhi P
    Commented Jul 31, 2023 at 22:18
0

On iOS 15.5 prefetching works fine, but on 14.5 it's broken with CompositionalLayout as you mention.

It only executes once (When the first batch of visible cells load), then the prefetch-delegate is never called again.

One thing to mention, in the docs Apple states it's not guaranteed to run for each possible indexPath. They "suggest" you implement an Operation based prefetching system. Where each indexPath has an operation for fetching the image. In other words, it's broken and they don't care.

You prob. also noticed if you create a cell full screen width with scrolling disabled (Meaning there will only be 1 only cell) the SDK will yet instantiate 2 cells (One of them will never be visible nor it show in the view hierarchy).

I swear iOS 14 is cursed. ios prefetch info

-3

I had the same problem like you, but I managed to solved. The dimensions of my layout are calculated in fractions, instead of estimated:

layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1.0),
          heightDimension: .fractionalWidth(2/3)))

and, in the delegate method prefetchItemsAt, you can use indexPaths to calculate the offset that you need to request on the server. For example:

var model : [YourModel] = []
collectionView.dataSource = self
collectionView.prefetchDataSource = self

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return model.count
    }

func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
        self.loadMore(for: indexPaths)
    }

func loadMore(for indexPaths: [IndexPath]) {
       for index in indexPaths where index.row >= (self.model.count - 1) {
           manager?.requestCharacters(withOffset: model.count, onSuccess: { [unowned self] (objects) in
                    self.model += objects
                    self.collectionView.reloadData()
                }, onError: { (error) in
                    print(error.localizedDescription)
                })
                return
            }
}

1
  • 1
    .fractionalHeight doesn't help in my app unfortunately, because each cell potentially has a different height, which is determined via auto layout constraints when the content is known.
    – Gereon
    Commented Jul 16, 2020 at 7:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.