GitHub

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

ScenarioMethodWhy
Data added, removed, or changedsetData(_:animatingDifferences:)Automatic diffing updates only what changed
Single row content changedsetData(_:animatingDifferences:)Diffing detects the change and reloads only affected cells
User typing in a cellremeasureRow(_:)Updates height without dismissing keyboard
Expandable/collapsible contentremeasureRow(_:)Height changes while cell stays on screen
Configuration changedreloadEverything()Clears stale layout cache
Sort order changedAutomaticSorting 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 setData once. 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: false to skip the diff calculation.
  • Throttle live edits — If remeasureRow is called on every keystroke, consider debouncing to reduce layout passes.