SeeMeCode
SeeMeCode

Reputation: 1435

UICollectionView cells only display after second call to reloadData

I'm probably doing something incredibly stupid, but I can't figure out for the life of me what's going on...

What I'm trying to do is quite simple. I have a UICollectionView dependent on data I'm loading from the server. After I get and set the data from the server, I call reloadData() on the collection view. When I do so, numberOfItemsInSection is called with the correct count, but cellForItemAt never gets called unless I call reloadData() for a second time (doesn't even need a delay).

Here's the general idea (although I realize it's likely related to some race conditional elsewhere, but maybe this will be enough to start the conversation):

class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

    @IBOutlet weak var collectionView: UICollectionView!

    var data = [DataModel]()

    override func viewDidLoad() {
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(UINib(nibName: "MyCell", bundle: nil), forCellWithReuseIdentifier: "MyCell")
        getData()
    }

    func getData() {
        someAsyncMethodToGetData() { (data) in
            // Using CloudKit so I think this is necessary, but tried it with and without
            DispatchQueue.main.async {
                self.data = data
                self.collectionView.reloadData()
                // This is the only way to get cells to render and for cellForItemAt to be called after data is loaded
                self.collectionView.reloadData()
            }
        }

    // Gets called initially AND after reloadData is called in the data completion closure. First time is 0, next time is number of DataModel objects
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }

    // Only gets called initially (unless I call reloadData() twice)
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = feedCollectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as? MyCell else {
            return UICollectionViewCell()
        }

        // Configure the cell with data...

        return cell
    }
}

So here are some things I've already tried:

Any suggestions? 🙏

Update 1

Ok, think I found the issue, but I'd like to understand the best way to solve. It appears by calling reloadData, the collection view will only reload cells already visible. I hacked the code above a bit to return 1 cell before getData() is called. When getData() then calls reloadData() that 1 cell gets updated (but if there are 10 items in the data array, it will still only [re]display the 1 cell). I suppose this makes sense, as reloadData() only reloads visible cells, but this has to be a common issue when the length of the array changes and there is still room on the screen to display. Also, why does calling reloadData() twice solve the issue and display cells that were never visible? 🤔

Here is an even simpler version of the code I have above, removing the whole "get data from a server" idea and replacing with a simple delay.

Upvotes: 8

Views: 5216

Answers (2)

First make sure if the delegate and the dataSource is correctly add to collectionView.

Second make sure if the data is fill whit breakpoint or a simple print

Third add this:

    DispatchQueque.main.async{
     self.collection.reloadData()
     self.collection.collectionViewLayout.invalidateLayout()
    }

Upvotes: 2

Roman Truba
Roman Truba

Reputation: 4401

The thing is UICollectionViewFlowLayout isn't invalidated correctly after number of items changed. So collectionView.contentSize is still old. You need to do something like

self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()

This will tell layout to recalculate for new data source.

Upvotes: 10

Related Questions