UICollectionViewDiffableDataSource creating duplicate cells

I'm working on a tvOS app using the new UICollectionViewDiffableDataSource to display up to 4 sections of groups of movies. If I'm scrolling the collectionView one poster at a time it works fine, but if I perform a quick swipe up or down between the sections, when we reach the top or bottom most cell a duplicate cell is made and 2 cells display in the same space overlapping each other. Here is the code I'm using to create the cells:


        if let collectionView = self.collectionView
        {
            self.dataSource = UICollectionViewDiffableDataSource.init(collectionView: collectionView, cellProvider: { (collectionView, indexPath, entity) -> UICollectionViewCell? in
                
                if let cell: PosterCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellIdentifier, for: indexPath) as? PosterCollectionViewCell
                {
                    print("Creating cell \(cell) for content named \(entity.title ?? "") index path \(indexPath) on Thread \(String(describing: Thread.current))")
                    cell.populate(withEntity: entity)
                    return cell
                }
                return UICollectionViewCell()
            })
        }


Here are a few screen shots of what it looks like visually when the duplicate cells are created:


I noticed on the print logs, whenever I scroll through the collectionView and the cell goes offscreen and gets recreated - it resuses the cell previously created each time (7 times in this case) EXCEPT when I perform the quick swipe. In that case, the same cell gets dequeued and then another immediately gets created with a new memory address. I assumed the closure on the UICollectionViewDiffableDataSource init method was being called on the main thread, but did a printout of the thread the cells were being created on just incase (and they obviously were), so I'm not sure why even if a new cell was being created it would duplicate the cell and overlay it instead of replacing it.


Creating cell PosterCollectionViewCell: 0x7fbc94011220; baseClass = UICollectionViewCell; frame = (10 1237; 248.5 330); opaque = NO; layer = CALayer: 0x600003d7cfe0 for content named Joker index path [3, 0] on Thread NSThread: 0x6000028ccbc0{number = 1, name = main}
Creating cell PosterCollectionViewCell: 0x7fbc91e1d7d0; baseClass = UICollectionViewCell; frame = (10 1218; 248.5 330); opaque = NO; layer = CALayer: 0x600003d95b20 for content named Joker index path [3, 0] on Thread NSThread: 0x6000028ccbc0{number = 1, name = main}


This only happens when I perform the quick swipe and land on a poster that is the top or bottom most on a section I can navigate to. It won't stop at just 2 either. If I continue the behavior, yet another cell gets created, you can see here in the view debugger we have 3 cells for the Joker movie in a section that should just be displaying the one cell:



Also, note that I am using the new UICollectionViewCompositionalLayout and am setting it up with this method

static func createLayout(posterHeight height: CGFloat, postersOnScreen numPosters: CGFloat, headerHeight: CGFloat = 0, edgeInsets: NSDirectionalEdgeInsets? = nil) -> UICollectionViewCompositionalLayout
    {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0 / numPosters),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .absolute(height))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        
        if let edgeInsets = edgeInsets
        {
            section.contentInsets = edgeInsets
        }
        
        if headerHeight > 0
        {
            let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                          heightDimension: .estimated(headerHeight))
            let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerFooterSize,
                elementKind:  UICollectionView.elementKindSectionHeader, alignment: .top)
            section.boundarySupplementaryItems = [sectionHeader]
        }
        return UICollectionViewCompositionalLayout(section: section)
    }


The cell duplication also seems to only happen when scrolling to a cell that was previously offscreen. When the cell comes on screen it performs the standard animation when it becomes focused and the duplicate is created.

Replies

Could you show the complete func from the first code snippet. In which func is it ?


Screenshots do not show on the forum.

That is the entirity of the method:

override func createDataSource()
    {
        if let collectionView = self.collectionView
        {
            self.dataSource = UICollectionViewDiffableDataSource.init(collectionView: collectionView, cellProvider: { (collectionView, indexPath, entity) -> UICollectionViewCell? in
                
                if let cell: PosterCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellIdentifier, for: indexPath) as? PosterCollectionViewCell
                {
                    print("Creating cell \(cell) for content named \(entity.title ?? "") index path \(indexPath) on Thread \(String(describing: Thread.current))")
                    cell.populate(withEntity: entity)
                    return cell
                }
                return UICollectionViewCell()
            })
        }
    }

This seems to be an issue with TVPosterView. If I remove the TVPosterView from my UICollectionView cell it no longer performs the expanding animation when the cell becomes focused, and no matter how quickly I scroll it, the cell never duplicates.

You override createDataSource.


Where was it first defined ? In a library ?

Hi !

I'm "Glad" I'm not alone having this issue.

I created a sample project with one simple collection view and an image in the cell and I reproduce the issue.


Here is the code:

import UIKit
import TVUIKit

class AViewController: UICollectionViewController {
    lazy var layout = createLayout()

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.setCollectionViewLayout(layout, animated: false)
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        10
    }
   
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    }

    func createLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { (section, _) -> NSCollectionLayoutSection? in
            let item = NSCollectionLayoutItem(
                layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                   heightDimension: .fractionalHeight(1)))

            let group = NSCollectionLayoutGroup.horizontal(
                layoutSize: NSCollectionLayoutSize(
                    widthDimension:  .absolute(350),
                    heightDimension: .absolute(300)),
                subitem: item,
                count:  1)

            let section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 50
            section.orthogonalScrollingBehavior =  .continuous

            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 40, trailing: 0)
            return section
        }
    }

To observe the issue you can just open the view hierarchy debugger after a few scrolls up and down and you should see the duplicated cells

It doesn't appear to come from the image as I reproduce the issue with a single label in my CollectionViewCell

Alright.


I created the collectionView and the cell programatically instead of using a storyboard CollectionViewController and I don't have the issue anymore 🤔. The search continues...


(here is the updated code)


//
//  ViewController.swift
//  testComposi
//
//  Created by Hugo Saynac on 08/12/2019.
//  Copyright © 2019 Hugo Saynac. All rights reserved.
//

import UIKit

class ColorCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
        let imageView = UIImageView(image:  imageLiteral(resourceName: "cat-smirk"))
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)
        addConstraints([
            imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
            imageView.topAnchor.constraint(equalTo: topAnchor),
            imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        imageView.adjustsImageWhenAncestorFocused = true
    }

    required init?(coder aDecoder: NSCoder) { fatalError("Unavailable") }
}

class AViewController: UIViewController, UICollectionViewDataSource {
    override func viewDidLoad() {
        super.viewDidLoad()

        createCollectionView()
    }

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    }

    fileprivate func createCollectionView() {
        let layout = createLayout()
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false

        view.addConstraints([
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        collectionView.register(ColorCell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.dataSource = self
    }
}

func createLayout() -> UICollectionViewLayout {
       UICollectionViewCompositionalLayout { (section, _) -> NSCollectionLayoutSection? in
           let item = NSCollectionLayoutItem(
               layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                  heightDimension: .fractionalHeight(1)))

           let group = NSCollectionLayoutGroup.horizontal(
               layoutSize: NSCollectionLayoutSize(
                   widthDimension:  .absolute(350),
                   heightDimension: .absolute(300)),
               subitem: item,
               count:  1)

           let section = NSCollectionLayoutSection(group: group)
           section.interGroupSpacing = 50
           section.orthogonalScrollingBehavior =  .continuous

           section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 40, trailing: 0)
           return section
       }
   }

extension UIColor {
    static var random: UIColor {
        return .init(hue: .random(in: 0...1), saturation: 1, brightness: 1, alpha: 1)
    }
}

Well, it looks like not using the storyboard removes the issue in my test project but not in my real project. 😟

Thanks for the reply and additional information/confirmation that it is broken. Definitely saves my sanity knowing it's happening to someone else as well. I can't believe no one else is running into this issue. Did you happen to file a radar for this issue already?
Also, between both of our projects it seems like the problem lies with using a UICollectionViewCompositionalLayout in combination with a cell that sets adjustsImageWhenAncestorFocused = true

Yes, createDataSource exists in the base class that this class that extends it overrides.

Oddly enough, if I set


self.posterView?.imageView.adjustsImageWhenAncestorFocused = false


the expanding animation (obviously) doesn't get performed, but the cell duplication still occurs. The search continues...

Is this the complete func ?


    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 
        collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) 
    }

That may not work. You dequeue a cell and don't know what's inside.

In some cases it still contains a previous cell content, hence the duplicate.


You should redefine the cell content here.

Thanks for your answer but the cell il empty, so the issue doesn't come from the content being duplicated. Plus looking at the view hierarchy I can see that the cell is is indeed duplicated and not its content.

Did anyone get a solution? I created my collection view programmatically but I get the same issue with there being 20 objects present but 30 cells in memory
Did anyone find the solution or issue causing this? We have an application, running on tvOS 13 and 14, and the issue only happens in tvOS 13. We don't have an idea of what could be causing this issue. Any help is appreciated.