Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • Made double sure reloadData() is being called on the main thread (added a symbolic breakpoint and called Thread.isMainThread to verify)
  • Moved reloadData() into a button action to eliminate the possibility of any thread issues - still had to click the button twice to get cells to show up
  • Put the updates in performBatchUpdates based on some other SO solutions I came across
  • Added setNeedsLayout and setNeedsDisplay (not sure why this would be necessary in this case, but I did try it out of desperation.

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.

like image 856
SeeMeCode Avatar asked Mar 19 '17 20:03

SeeMeCode


2 Answers

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.

like image 172
Roman Truba Avatar answered Oct 09 '22 18:10

Roman Truba


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()
    }
like image 35
Gerardo Salazar Sánchez Avatar answered Oct 09 '22 20:10

Gerardo Salazar Sánchez