Incremental Updates
Update individual rows or cells without reloading the entire table.
When data changes, you don't want to reload the entire table. SwiftDataTables provides several ways to update efficiently — from automatic cell-level diffing to manual height remeasurement for live editing.
The Primary Approach: setData with Diffing
For most updates, setData(_:animatingDifferences:) is all you need. It automatically calculates what changed and updates only the affected cells.
// Add a new employee
employees.append(newEmployee)
dataTable.setData(employees, animatingDifferences: true)
// Result: Only the new row animates in. Existing rows untouched.
// Update one employee's department
employees[5].department = "Engineering"
dataTable.setData(employees, animatingDifferences: true)
// Result: Only the "Department" cell in row 5 reloads. Other cells untouched.
// Remove several employees
employees.removeAll { $0.isInactive }
dataTable.setData(employees, animatingDifferences: true)
// Result: Removed rows animate out. Remaining rows shift up smoothly.What Happens Under the Hood
- Identity comparison — The table compares old and new data using
Identifiable.id. Rows with the same ID are considered the same row. - Row-level diff — Rows are classified as inserted, deleted, moved, or unchanged based on their IDs.
- Cell-level diff — For unchanged rows (same ID, possibly different content), each column value is compared. Only changed cells reload.
- Animated batch update — Insertions, deletions, and reloads are batched into a single animated transaction.
This means updating one cell in a 50,000-row table only reloads that one cell — not the entire table, not even the entire row.
Live Editing: remeasureRow
When a user is actively editing content — typing in a text field, expanding a section — you need to update the row height without reloading the cell. Reloading would dismiss the keyboard and lose focus.
remeasureRow(_:) solves this. It recalculates the row height based on the current cell content, updates the layout, and leaves the cell itself untouched.
class EditableCell: UICollectionViewCell, UITextViewDelegate {
var textView: UITextView!
var onContentChange: ((String) -> Void)?
func textViewDidChange(_ textView: UITextView) {
// Notify the view controller that content changed
onContentChange?(textView.text)
}
}
// In your view controller
func configureCell(_ cell: EditableCell, at row: Int) {
cell.textView.text = notes[row].content
cell.onContentChange = { [weak self] newText in
guard let self = self else { return }
// Update your model
self.notes[row].content = newText
// Remeasure the row — keyboard stays up, cell stays focused
self.dataTable.remeasureRow(row)
}
}What remeasureRow Does
- Measures visible cells — Uses the actual on-screen cells (which have the current content) rather than sizing cells (which have old data).
- Compares heights — If the new height differs from the cached height by more than 0.5 points, proceeds with update.
- Updates metrics — Stores the new height and recalculates Y-offsets for all rows below.
- Invalidates layout — Triggers a layout pass without reloading cells. The cell stays on screen, keyboard stays up.
Automatic heights required remeasureRow only works when using .automatic row height mode. With .fixed heights, there's nothing to remeasure.
Configuration Changes: reloadEverything
When you change configuration options at runtime — toggling the footer, changing column width mode, modifying sort settings — the layout cache may become stale. Use reloadEverything() to force a complete refresh.
// Toggle footer visibility
dataTable.options.shouldShowFooter = false
dataTable.reloadEverything()
// Change column width strategy
dataTable.options.columnWidthMode = .fixed(width: 100)
dataTable.reloadEverything()This clears the layout cache and reloads all cells. It's heavier than setData, so only use it when configuration actually changes — not for data updates.
Choosing the Right Approach
| Scenario | Method | Why |
|---|---|---|
| Data added, removed, or changed | setData(_:animatingDifferences:) | Automatic diffing updates only what changed |
| Single row content changed | setData(_:animatingDifferences:) | Diffing detects the change and reloads only affected cells |
| User typing in a cell | remeasureRow(_:) | Updates height without dismissing keyboard |
| Expandable/collapsible content | remeasureRow(_:) | Height changes while cell stays on screen |
| Configuration changed | reloadEverything() | Clears stale layout cache |
| Sort order changed | Automatic | Sorting calls reload internally |
Scroll Position Preservation
Both setData and remeasureRow preserve scroll position. When rows are inserted above the visible area, or when row heights change, the table adjusts its content offset to keep the user's view stable — no jarring jumps.
This is handled automatically through scroll anchoring:
- Insertions above viewport — Content offset increases to compensate
- Deletions above viewport — Content offset decreases
- Height changes — Offset adjusts based on anchor row position
Performance Tips
- Batch your changes — Make multiple model changes, then call
setDataonce. Don't call it after each change. - Use Identifiable correctly — Stable IDs enable efficient diffing. If IDs change unnecessarily, rows get deleted and re-inserted instead of updated.
- Avoid animatingDifferences for bulk imports — When loading thousands of rows initially, use
animatingDifferences: falseto skip the diff calculation. - Throttle live edits — If
remeasureRowis called on every keystroke, consider debouncing to reduce layout passes.