cellForRowAtIndexPath is being called for non visible cells also - ios

tableview data source method cellForRowAtIndexPath is being called for the non visible rows also which is creating a problem in pagination in tableview. Did anyone face this issue?

The system calls this method to prepare cells ahead of time, so scrolling is smoother and more responsive. If you want to fetch more data as the user scrolls down/up you should rather implement UITableViewDelegate method tableView(_:willDisplay:forRowAt:) - this method is called just before the cell is displayed to the user.

Related

Getting loaded vs visible cells on a UITableView or UICollectionView

With the introduction of iOS 10, it seems like we're going to have prefetching enabled by default on UITableView and UICollectionViews. This means that cells that aren't displayed on the screen are going to be fetched before the user actually sees them.
Here are some relevant methods:
UITableView:
cellForRowAtIndexPath:: returns "nil if the cell is not visible."
visibleCells: each item represents "a visible cell in the table view."
indexPathsForVisibleRows: each item represents "a visible row in the table view."
UICollectionView:
visibleCells: "returns the complete list of visible cells displayed by the collection view."
indexPathsForVisibleItems: each item represents "a visible cell in the collection view."
cellForItemAtIndexPath:: returns "nil if the cell is not visible."
All these specifically mention "visible" in their descriptions. With the introduction of pre-fetching in iOS 10, how would I distinguish between a cell that was pre-fetched vs. one that is currently visible?
In other words:
How do I get all visible cells?
How do I get all loaded cells?
It does not look like there are any new APIs on either UITableView or UICollectionView that can help with this.
TL;DR
Take the visible in function names literally.
UITableView behaves just as it did in iOS 9.
You'll need to do some bookkeeping if you want to treat loaded vs. visible cells differently in UICollectionView on iOS 10.
UITableView and UICollectionView appear to behave very differently when it comes to prefetching.
First thing to notice is that there is a difference between prefetching cells and prefetching data:
Prefetching cells refers to the cellForRowAtIndexPath being called before the cell is actually displayed on screen. This enables the scenario where you have cells that are off-screen but still loaded.
Prefetching data refers to the prefetchDataSource methods which inform you about indexPaths that are going to be displayed on screen. You do not have a reference to the cell when this method is called, and you do not return a cell when this method is called. Instead, this method should do things like fire off a network request to download an image that will be displayed in the cell.
Note: In all of these scenarios, imagine there are 8 cells that can be displayed at any given time.
UITableView: (options: no prefetching, or prefetch data)
Does not prefetch cells, ever. In other words, it will never call cellForRowAtIndexPath on an indexPath that isn't displayed.
As such, there is no isPrefetchingEnabled property on a UITableView.
You can opt-in to prefetching data by using the prefetchDataSource.
Note that although the table view does seem to be less aggressive with reusing cells, it still appears to call cellForItemAtIndexPath when the reused cell comes back on screen. (Although I may need to do some more investigation as to this, especially for collection views.)
UICollectionView: (options: no prefetching, prefetch cells, or prefetch cells and data)
Prefetches cells by default. In other words, it will call cellForItemAtIndexPath for cells that aren't going to be immediately displayed.
The prefetching of cells only begins when the user scrolls up or down on the collection view. In other words, you will get exactly 8 calls to cellForItemAtIndexPath when the view is loaded. Only once the user scrolls down will it start asking for non-visible cells (e.g. if you scrolled down to show 2-10, it might ask for 11-14).
When the prefetched, non-visible cell comes on screen, it's not going to call cellForItemAtIndexPath again. It's going to assume that instantiation you did the first time is still valid.
You can opt-in to prefetching data by using the prefetchDataSource.
The prefetchDataSource turns out to be only useful for the initial load. In the same scenario above, when the first 8 cells are displayed, it may fire off a prefetching of data for cells 9-14, for example. However, once this initial method is called, it's useless thereafter. This is because cellForItemAtIndexPath is going to be called immediately after each call to prefetchItemsAt. For example, you'll get prefetchItemsAt:[14, 15] immediately followed by cellForItemAt:14, cellForItemAt:15.
You can opt-out of all prefetching behavior by setting isPrefetchingEnabled = false. This means you can't make a UICollectionView behave similarly to a UITableView with a prefetchDataSource. Or, in other words, you can not have a UICollectionView prefetch data only.
For both:
visibleCells, indexPathsForVisibleRows, and cellForItemAtIndexPath do exactly as they say: they only deal with visible cells. In our same scenario, if we have 20 cells loaded, but only 8 are visible on screen. All 3 of these methods will only report about the 8 on-screen cells.
So what does this mean?
If you're using a UITableView, you can use it as is and never have to worry about a difference between loaded vs. visible cells. They are always equivalent.
For UICollectionView, on the other hand, you're gonna need to do some book-keeping to keep track of loaded, non-visible cells vs. visible cells if you care about this difference. You can do this by looking at some of the methods on the data source and delegate methods (e.g. willDisplayCell, didEndDisplayingCell).

Should I set the cell on cellForItemAtIndexPath or willDisplayCell?

this page that talks about tricks to smooth table view scrolling, says the table view cells should be drawn not on tableView:cellForRowAtIndexPath: but rather on tableView:willDisplayCell:forRowAtIndexPath:.
I have tried that approach on and I see no difference on my initial tests. Is there any truth about that?
should I really use tableView:willDisplayCell:forRowAtIndexPath: to create the cell, set thumbnails, texts, etc. instead of tableView:cellForRowAtIndexPath:?
Yes its true that both perform the same function but use cellForRowAtIndexPath for tableview where datasource has to be implemented as cellForRowAtIndexPath works fast and you must return reused cell identifiers quickly. This is a method of datasource of tableview
tableView:willDisplayCell:forRowAtIndexPath is a method of delegate & not datasource. This method is called exactly before loading cells in UITableView bounds.

In UITableView, what's the delegate for "visibleCells"?

When cells come in and out of device's screen, I want my viewController to know exactly what came and what went out. Is there a way to do this?
There isn't a delegate method just for "visible cells". There isn't anything called when a cell leaves the screen. There really isn't anything when a cell becomes visible.
There is the cellForRowAtIndexPath data source method. This is called when a cell is needed.
There is the willDisplayRowAtIndexPath delegate method. This is called when a cell will be displayed.
There is the didEndDisplayingCell delegate method. This is called when a cell is removed from the table view.
There is the indexPathsForVisibleRows method on UITableView. This lets you know what rows are currently in view.
There is the prepareForReuse method on UITableViewCell. This lets a cell reset itself to be reused for another row.
Better describe what you are trying to accomplish in order to get a more specific answer.

How do I perform cell selection animations while using reloadData?

I have a UICollectionView, and I override setSelected in the UICollectionViewCell subclass to perform a little bounce animation when the user selects the cell.
The problem is that on selection of the cell, the very selection of the cell alters the data for the other cells (for instance, imagine the other cells have a label in each one that has the amount of cells selected displayed). So I call collectionView.reloadData() so that all the other cells update as well to show the new data (for instance, the amount of cells selected in a label).
However, this reloadData() call seems to reset the UICollectionView completely. Any animations taking place are stopped and the cells are simply updated without animation.
How do I have the cell selection animation, and update the other cells at the same time?
I've tried calling reloadData() only after the animation has completed, but this makes it look like there's lag/delay in updating the other cells (as it doesn't update them until a moment after when the animation finishes), which doesn't look very good.
Similarly, I've tried calling reloadItemsAtIndexPaths: (and exclude the selected cell that will animate), but for whatever reason with collection views this method seems really slow. Like there's a noticeable amount of lag after pressing it.
What should I be doing here? What's the standard for reloading data without the collection view destroying in-progress animations?
Reloading throws away the existing cells and creates (or reuses) new ones. So any animation is inherently destroyed. So, don't reload.
Instead, update your data model as usual and then get the currently visible index paths and associated cells and directly update those cells.

UITableView initial row selection

Maybe I'm missing something obvious, but I'm have a hard time programmatically setting the row selection in a tableView. The goal is to simply have a tableView open with a row already selected. The problem appears to be that I have to wait until the tableView is fully loaded before I can modify the selection.
I've read various strategies such as calling reloadData for the tableView in the viewController's viewWillAppear method, then immediately calling selectRowAtIndexPath for the target row. But when I do that, I get a range exception because the tableView has zero rows at that point. The UITableViewDelegate methods (numberOfRowsInSection, etc.) don't appear to be called immediately in response to reloadData (which makes sense if the table rows are drawn "lazily").
The only way I've been able to get this to work is to call selectRowAtIndexPath after a short delay, but then you can see the tableView scroll the selected row into view.
Surely, there's a better way of doing this?
Well, you can use another strategy. You can create a hidden table view, configure how you want and than show to user. Use the tableview.hidden = YES.

Resources