Do You Know (iOS 15) Table View?
How well do you know about iOS's table view? Did you know they added a new feature in iOS 15?
By ahchao in Learning Swift
October 31, 2021
How well do you know UITableView?
iOS Table View has been around for a long time since iOS 2.0+.
The concept of table view is to reuse cells (you can take it as a row = a cell).
Basic idea is this:
If the tableview can fit in 10 cells, but you only have 5 rows, it will create 5 rows in memory. If your data grows to show > 10 rows, it will create another 6 more cells (because you can have 11 cells being visible at once: the top and last row are partially visible), and keep reusing those pool of 11 cells. We can’t guarantee in what/which order the cells are being reused, but it doesn’t matter. It doesn’t destroy the object nor create extra for no reason, and we all know repeatedly allocating/deallocating memory is expensive and hurts performance. Quite simple, but I wonder how many iOS devs doesn’t know this.
2 more basic things we should know:
- When a cell is about to come into view (becoming visible/onscreen), tableView(_:cellForRowAt: is called.
- When a cell is being hidden (becoming non-visible/offscreen), tableView(_:didEndDisplaying:forRowAt:) is called.
Now that we have a quick recap, one of the project I am in, has code similar to the following. Assume no crash, code runs and everything is linked properly, and the table view is showing up on screen (code is probably weird but I had to simplify it, so bear with me). What the code is trying to do, is to basically show 50 rows and each has their row index number set, except for firstmost row (at index 0), where we want to set some text.
See if you think there will be a potential bug or not:
Hopefully the above code is easily understandable.
I think based on the above, it is easy for you (I hope!) to guess where a potential problem is, and how to solve it.
For those who are not familiar with iOS, since cells are being recycled, and since there is nowhere where
cell.rightLabel.text is being reset to empty string, hence the text “This is only shown for index 0.” will appear in more than 1 row as the user scrolls down:
Once the cell is modified, the change is there forever, unless we reset it back.
This is easily fixable, by something like the following:
- Add more code to reset it when it is becoming invisible:
- Just set it directly in the cellForRow function when becoming visible:
You can download the example project here: TableViewPrefetch
The ‘new’ iOS 15.0 UITableView
iOS 15.0 table view introduced a new concept called cell prefetching, not to be confused with data prefetching which already existed in iOS 10 (along with collection view). This cell prefetching is nothing new, and collectionView already has since it since iOS 10.0 on top of data prefetching, but with a new behaviour. I am not familiar with the internal details, but it basically prefetch the next few cells that is going to appear (probably based on your scrolling direction) in advance, so it has up to 2x more time to do the work needed to display the cell, before it becomes visible. Best if you watch both WWDC videos above.
However with this new tableview change, I notified that
tableView(_:cellForRowAt: is no longer called. Apparently back in iOS 10 for collection view, with this prefetch on, the lifecycle is changed such that if say, the last cell disappears and reappears again,
cellForItem(at:) is not called, but
willDisplayCell will still be called. So I guess you can say it is ‘cached’. I don’t know the extend of this, but this had NEVER happen for table view before, and hence did not expected this kind of behaviour until now.
I initially thought this was a bug of iOS 15.0, since
tableView(_:cellForRowAt: was not called when it go off screen and reappeared, but apparently not. It is now no longer guaranteed that the cell goes back into the reuse pool when it ends displaying, assuming you are on iOS 15 with
isPrefetchingEnabled = true.
This causes a problem to my current project, where our labels are disappearing randomly for no apparent reason, and I found out that our code were dependent on
tableView(_:didEndDisplaying:forRowAt:) being called (to delete the labels in the cell) followed by
tableView(_:cellForRowAt: (to add back the new labels in the cell) when it reappears back on screen. Now that we know calls to
tableView(_:cellForRowAt: is not guaranteed, there is a mismatch in the calling sequence which the coder had assumed, and it means labels might not show up in our cells anymore, hence the ‘randomly-disappearing-labels-and-randomly-reappearing-back’ bug we are seeing.
Here is a replicable code that illustrates what I am talking about. Run it on iOS 15 -
true by default. Scroll up the cell row to make it disappear (end display), then release it so it bounces back into view but notice
tableView(_:cellForRowAt: doesn’t get called as we always would for iOS versions < 15:
With the above information in hand, we can turn this off via
tableView.isPrefetchingEnabled = false, or rework our code such that the displaying of information in the cells is not dependent on
tableView(_:cellForRowAt: to be called in pairs. If prefetching is on moving forward to take advantage of the performance, looks like we also need to use
reconfigureRows as well now, though I am still figuring out what is the difference between that and directly calling
cellForRow if I need to update the cell when the data changes. Anyone knows? Do point me in the right direction!
I just found this twitter thread while writing this post, which I thought is quite good at talking about it: https://twitter.com/smileyborg/status/1406769460032114689?lang=en (especially this: https://twitter.com/smileyborg/status/1406769463253364736). It also brought up a point where if the cell is cached (not updated) but underlying data has changed, then we have a problem. A good summary of what I said basically.
The above is based on my experience with table views and understanding of the documentation and various sources on the internet.
Please feel free to correct me if I am wrong, let’s learn together!