UICollectionView with Compositional Layout & Diff Datasources disappears on scroll

Viewed 682

I currently have a UICollectionView which I'm using compositional layouts and Diffable Data sources. I'm not doing anything crazy just loading 150 cells into it and defining a 4 column layout. I seem to be encountering some weird behaviour whenever I tap the collectionview to scroll the items it seems to be disappearing... Below is my file with the entire code that I'm using so that you can copy & paste this and see this weird behaviour. Does anyone know why this might happening?

import UIKit

class ViewController: UIViewController {
    
    private lazy var myCollectionViewLayout = MyCollectionViewLayout()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        // Do any additional setup after loading the view.
    }
}

private extension ViewController {
    
    func setup() {
        
        let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
        collectionVw.translatesAutoresizingMaskIntoConstraints = false
        collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)

        self.view.addSubview(collectionVw)
        
        NSLayoutConstraint.activate([
            collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
            collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
            collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
        ])
        
        let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
            cell?.configure(at: indexPath.row)
            return cell
        }

        collectionVw.dataSource = dataSource
        
        var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()

        snapshot.appendSections([0])

        Range(0...150).forEach { item in
            snapshot.appendItems([UUID()], toSection: 0)
        }

        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

class MyCollectionViewLayout {
    
    func createLayout() -> UICollectionViewCompositionalLayout {
        
        let layout =  UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
                                                  heightDimension: .fractionalHeight(1))
                
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                   heightDimension: .fractionalWidth(0.25))
            
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

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

            let section = NSCollectionLayoutSection(group: group)

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

            return section
        }
        
        return layout
        
    }
}

class MyCustomCell: UICollectionViewCell {
    
    static let cellId = "MyCustomCell"
    
    private var lbl: UILabel?
    
    func configure(at index: Int) {
        
        self.contentView.layer.cornerRadius = 8
        self.contentView.backgroundColor = .blue
        
        lbl = UILabel()
        lbl?.translatesAutoresizingMaskIntoConstraints = false
        lbl?.text = index.description
        lbl?.textAlignment = .center
        lbl?.font = .preferredFont(forTextStyle: .headline)
        
        self.contentView.addSubview(lbl!)
        
        NSLayoutConstraint.activate([
            lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
            lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
            lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
            lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
        ])
        
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        lbl?.removeFromSuperview()
    }
}
1 Answers

The reason is that your data source is a local variable inside your setup method (let dataSource...). Therefore it works just the one time to populate the collection view initially, and then vanishes in a puff of smoke, leaving the collection view with no data.

The data source needs to be a property of the view controller so that it persists! Once you make that change, all will be well.

It's actually a little tricky to do that, because the data source depends on the collection view. So you have to use lazy instantiation. You create the collection view with an empty layout and hook everything together in the first step of the setup.

Here's a minimal rewrite of your code; look for my comments explaining the changes.

import UIKit


class ViewController: UIViewController {
    // delete; this is pointless
    // private lazy var myCollectionViewLayout = MyCollectionViewLayout()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
    
    // need persistent pointer to collection view
    let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())

    // need persistent pointer to data source
    lazy var dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
        cell?.configure(at: indexPath.row)
        return cell
    }

}

private extension ViewController {
    
    func setup() {
        
        
//        let collectionVw = UICollectionView(frame: .zero, collectionViewLayout: myCollectionViewLayout.createLayout())
        // everything is persistent, now just hook them together
        collectionVw.collectionViewLayout = MyCollectionViewLayoutMaker.createLayout()
        
        collectionVw.translatesAutoresizingMaskIntoConstraints = false
        collectionVw.register(MyCustomCell.self, forCellWithReuseIdentifier: MyCustomCell.cellId)

        self.view.addSubview(collectionVw)
        
        NSLayoutConstraint.activate([
            collectionVw.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
            collectionVw.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
            collectionVw.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            collectionVw.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
        ])
        
//        let dataSource = UICollectionViewDiffableDataSource<Int, UUID>(collectionView: collectionVw) { (collectionView, indexPath, item) -> UICollectionViewCell? in
//
//            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.cellId, for: indexPath) as? MyCustomCell
//            cell?.configure(at: indexPath.row)
//            return cell
//        }

        collectionVw.dataSource = dataSource
        
        var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()

        snapshot.appendSections([0])

        Range(0...150).forEach { item in
            snapshot.appendItems([UUID()], toSection: 0)
        }

        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

class MyCollectionViewLayoutMaker {
    // make this static; the class is not the layout, it serves no purpose except as a factory
    static func createLayout() -> UICollectionViewCompositionalLayout {
        
        let layout =  UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnv: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
                                                  heightDimension: .fractionalHeight(1))
                
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                   heightDimension: .fractionalWidth(0.25))
            
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

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

            let section = NSCollectionLayoutSection(group: group)

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

            return section
        }
        
        return layout
        
    }
}

class MyCustomCell: UICollectionViewCell {
    
    static let cellId = "MyCustomCell"
    
    private var lbl: UILabel?
    
    func configure(at index: Int) {
        
        self.contentView.layer.cornerRadius = 8
        self.contentView.backgroundColor = .blue
        
        lbl = UILabel()
        lbl?.translatesAutoresizingMaskIntoConstraints = false
        lbl?.text = index.description
        lbl?.textAlignment = .center
        lbl?.font = .preferredFont(forTextStyle: .headline)
        
        self.contentView.addSubview(lbl!)
        
        NSLayoutConstraint.activate([
            lbl!.topAnchor.constraint(equalTo: self.contentView.topAnchor),
            lbl!.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
            lbl!.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
            lbl!.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
        ])
        
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        lbl?.removeFromSuperview()
    }
}
Related