How does a UITableView get its rows, and why do batch updates crash when the data and the update disagree?
Predict first
What is the default row height setting?
import UIKit
print(UITableView().rowHeight == UITableView.automaticDimension)
- true
- false
Predict first
What happens when you insert a row but the data source still reports zero rows?
import UIKit
final class Source: NSObject, UITableViewDataSource {
var rows = 0
func tableView(_ t: UITableView, numberOfRowsInSection s: Int) -> Int { rows }
func tableView(_ t: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell {
t.dequeueReusableCell(withIdentifier: "cell", for: i)
}
}
let source = Source()
let table = UITableView(frame: CGRect(x: 0, y: 0, width: 300, height: 600))
table.dataSource = source
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
table.reloadData()
table.layoutIfNeeded()
table.insertRows(at: [IndexPath(row: 0, section: 0)], with: .none)
print("survived")
- One row appears
- It crashes: the row count after the update does not match
Predict first
What happens when a cell is dequeued with an identifier that was never registered?
import UIKit
final class Source: NSObject, UITableViewDataSource {
var rows = 3
func tableView(_ t: UITableView, numberOfRowsInSection s: Int) -> Int { rows }
func tableView(_ t: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell {
t.dequeueReusableCell(withIdentifier: "cell", for: i)
}
}
let source = Source()
let table = UITableView(frame: CGRect(x: 0, y: 0, width: 300, height: 600))
table.dataSource = source
table.reloadData()
table.layoutIfNeeded()
print("survived")
- A default cell is made
- It crashes: must register a class or nib
Mental model. The table is a clerk who counts your stock. If you say “I added an item” but the stock count did not change, the clerk raises an alarm.
A table view asks its data source for the number of sections and rows and for a cell per index path, and asks its delegate about behaviour (selection, row heights, swipe actions, headers). You register a cell class or nib under an identifier and call dequeueReusableCell(withIdentifier:for:); with the for: form it never returns nil, and it crashes if the identifier was never registered. Heights come from rowHeight, which defaults to UITableView.automaticDimension, so rows size themselves from their constraints, with an estimatedRowHeight to keep scrolling smooth.
To change what is shown you either call reloadData() (simple, but animates nothing and discards state such as selection and scroll position of cells) or you update the model and tell the table which rows changed with insertRows, deleteRows, reloadRows and moveRow, usually inside performBatchUpdates. Those calls make the table check your arithmetic: after the update, the row count your data source returns must equal the old count plus insertions minus deletions. If it does not, UIKit raises an exception such as “attempt to insert row 0 into section 0, but there are only 0 rows after the update”. Change the model first, then call the matching table method, in the same batch. This whole class of crash disappears with a diffable data source, which computes the changes for you.
Rows size themselves by default. You only set a fixed height when all rows should be identical.
The table checks that old count + insertions equals the new count. Zero rows after inserting one is inconsistent, so UIKit raises an exception.
The for: variant promises a cell, so with nothing registered UIKit raises an exception.
Common traps
- Calling
insertRowswithout changing the data source first. The counts disagree and it crashes. - Calling
dequeueReusableCellwithout registering. It crashes with “unable to dequeue a cell with identifier”.
They might follow up with
- What does the non-
for:dequeue return? An optional, nil when no cell is available, so you must create one yourself. - How do swipe actions work? The delegate returns
UISwipeActionsConfigurationfor leading and trailing edges.
Related questions
- 7.5.2 How does a diffable data source work, and what does it require of your item identifiers?
- 7.5.3 How does cell reuse work, and what are the classic reuse bugs?
- 7.6.2 Table view or collection view: which do you choose for new code?
Table view or collection view: which do you choose for new code? →