To know when a table view finishes loading its content, we first need to have a basic understanding of how the views are put on screen.
In the life cycle of an app, there are 4 key moments :
- The app receives an event (touch, timer, block dispatched etc)
- The app handles the event (it modifies a constraint, starts an animation, changes background etc)
- The app computes the new view hierarchy
- The app renders the view hierarchy and displays it
The 2 and 3 times are totally separated. Why ? For performance reasons, we don't want to perform all the computations (done at 3) each time a modification is done.
So, I think you are facing a case like this :
tableView.reloadData()
tableView.visibleCells.count // wrong count oO
What’s wrong here?
A table view reloads its content lazily. Actually, if you call reloadData
multiple times it won’t create performance issues. The table view only recomputes its content size based on its delegate implementation and waits the moment 3 to loads its cells. This time is called a layout pass.
Okay, how to get involved in the layout pass?
During the layout pass, the app computes all the frames of the view hierarchy. To get involved, you can override the dedicated methods layoutSubviews
, updateLayoutConstraints
etc in a UIView
subclass and the equivalent methods in a view controller subclass.
That’s exactly what a table view does. It overrides layoutSubviews
and based on your delegate implementation adds or removes cells. It calls cellForRow
right before adding and laying out a new cell, willDisplay
right after. If you called reloadData
or just added the table view to the hierarchy, the tables view adds as many cells as necessary to fill its frame at this key moment.
Alright, but now, how to know when a tables view has finished reloading its content?
We can rephrase this question: how to know when a table view has finished laying out its subviews?
• The easiest way is to get into the layout of the table view :
class MyTableView: UITableView {
func layoutSubviews() {
super.layoutSubviews()
// the displayed cells are loaded
}
}
Note that this method is called many times in the life cycle of the table view. Because of the scroll and the dequeue behavior of the table view, cells are modified, removed and added often. But it works, right after the super.layoutSubviews()
, cells are loaded. This solution is equivalent to wait the willDisplay
event of the last index path. This event is called during the execution of layoutSubviews
of the table view when a cell is added.
• Another way is to be notified when the app finishes a layout pass.
As described in the documentation, you can use an option of the UIView.animate(withDuration:completion)
:
tableView.reloadData()
UIView.animate(withDuration: 0) {
// layout done
}
This solution works but the screen will refresh once between the time the layout is done and the time the block is executed. This is equivalent to the DispatchMain.async
solution but specified.
• Alternatively, I would prefer to force the layout of the table view
There is a dedicated method to force any view to compute immediately its subview frames layoutIfNeeded
:
tableView.reloadData()
table.layoutIfNeeded()
// layout done
Be careful however, doing so will remove the lazy loading used by the system. Calling those methods repeatedly could create performance issues. Make sure that they won’t be called before the frame of the table view is computed, otherwise the table view will be loaded again and you won’t be notified.
I think there is no perfect solution. Subclassing classes could lead to trubles. A layout pass starts from the top and goes to the bottom so it’s not easy to get notified when all the layout is done. And layoutIfNeeded()
could create performance issues etc.