Repository navigation
Summary
The most significant update since the library's inception. Modernised architecture with iOS 17+ minimum, type-safe generics, automatic diffing, and 928x performance improvement.
Performance: 928x Faster
| Scenario | Before v0.9.0 | After v0.9.0 |
|---|---|---|
| 1,000 rows | ~2 seconds | Instant (<0.1s) |
| 10,000 rows | ~23 seconds | ~0.25 seconds |
| 50,000 rows | ~232 seconds (4 min) | ~0.25 seconds |
| 100,000 rows | Would timeout/crash | ~0.5 seconds |
No code changes required — optimizations are enabled by default.
What Changed
- O(n) Layout Algorithm (was O(n²)) — Pre-calculate Y-offsets once, cache column widths and row heights
- Estimated Column Widths — Character count estimation (
width = chars × 7pt) instead of font measurement - Row Height Caching — Heights calculated once per data update
Type-Safe Columns API
The Problem with Raw Arrays
Previously, manual conversion was required:
// Old approach - verbose and error-prone
let data: [[DataTableValueType]] = users.map { user in
[.string(user.name), .int(user.age), .double(user.score)]
}
let table = SwiftDataTable(data: data, headerTitles: headers)The New Typed Approach
struct User: Identifiable {
let id: Int
let name: String
let age: Int
}
let table = SwiftDataTable(data: users, columns: [
.init("Name", \.name),
.init("Age", \.age)
])DataTableColumn Options
// KeyPath extraction
DataTableColumn("Name", \.name)
// Custom extraction - return any DataTableValueConvertible directly
DataTableColumn("Full Name") { "\($0.firstName) \($0.lastName)" }
DataTableColumn("Salary") { "£\($0.salary)" }
// Header-only for custom cell columns
DataTableColumn<User>("Actions")Supported Types
| Type | Conversion |
|---|---|
String |
.string(value) |
Int |
.int(value) |
Float |
.float(value) |
Double |
.double(value) |
Optional<T> |
Wrapped value or empty string |
Extend for custom types:
extension Date: DataTableValueConvertible {
public func asDataTableValue() -> DataTableValueType {
.string(formatted(date: .medium, time: .omitted))
}
}Animated Diffing
Automatic Updates
users.append(User(id: 4, name: "Diana", age: 28))
users.removeAll { $0.id == 2 }
users[0] = User(id: 1, name: "Alice Updated", age: 31)
// Single call to animate all changes
table.setData(users, animatingDifferences: true)Cell-Level Updates
Only changed cells reload, not entire rows:
- Better performance — Less re-rendering work
- No flicker — Unchanged cells remain stable
- Smoother animations — Only affected cells animate
Model Access
// Single row
if let user: User = table.model(at: indexPath.row) {
print("Selected: \(user.name)")
}
// All models
if let users: [User] = table.allModels() {
print("Total: \(users.count)")
}Column Sortability Control
Per-Column Sorting
var config = DataTableConfiguration()
// Disable sorting for specific columns
config.isColumnSortable = { columnIndex in
columnIndex != 0 // First column not sortable
}
// Hide sort indicator but keep sorting enabled
config.shouldShowSortingIndicator = { columnIndex in
columnIndex != 2 // Hide arrow on column 2
}Header Tap Delegate
func dataTable(_ dataTable: SwiftDataTable, didTapHeaderAt columnIndex: Int) {
// Called before sorting occurs
// Use with isColumnSortable for custom header tap handling
}Typed Sorting Initializers
Format values for display while preserving typed sorting:
// Format for display, sort by underlying value
DataTableColumn("Salary", \.salary) { "$\(String(format: "%.2f", $0))" }
// Sort by one property, display computed value
DataTableColumn("Name", sortedBy: \.lastName) { "\($0.first) \($0.last)" }
// Sort by computed value
DataTableColumn("Title", sortedBy: { $0.title.count }) { $0.title }
// Custom comparator (nulls last, case-insensitive, etc.)
DataTableColumn("Due", sortedBy: { lhs, rhs in
switch (lhs.dueDate, rhs.dueDate) {
case (nil, nil): return .orderedSame
case (nil, _): return .orderedDescending
case (_, nil): return .orderedAscending
case (let a?, let b?): return a.compare(b)
}
}) { $0.dueDate?.formatted() ?? "No date" }Self-Sizing Cells & Auto Layout
Automatic Row Heights
var config = DataTableConfiguration()
config.textLayout = .wrap
config.rowHeightMode = .automatic(estimated: 44)Custom Cell Provider
config.cellSizingMode = .autoLayout(provider: DataTableCustomCellProvider(
register: { collectionView in
collectionView.register(UserCardCell.self, forCellWithReuseIdentifier: "UserCard")
},
reuseIdentifierFor: { indexPath in
indexPath.item == 0 ? "UserCard" : "Default"
},
configure: { cell, value, indexPath in
(cell as? UserCardCell)?.configure(with: value.stringRepresentation)
},
sizingCellFor: { reuseId in
reuseId == "UserCard" ? UserCardCell() : nil
}
))Live Cell Editing with remeasureRow()
func textViewDidChange(_ textView: UITextView) {
notes[rowIndex].content = textView.text
dataTable.remeasureRow(rowIndex) // No cell reload, keyboard stays up
}Column Width Strategies
var config = DataTableConfiguration()
config.columnWidthMode = .fitContentText(strategy: .hybrid(sampleSize: 100, averageCharWidth: 7))| Strategy | Description | Performance |
|---|---|---|
.estimatedAverage(averageCharWidth:) |
Character count × width estimate | Fastest |
.maxMeasured |
Measure every cell, use maximum | Most accurate |
.sampledMax(sampleSize:) |
Measure sample, use maximum | Balanced |
.percentileMeasured(percentile:, sampleSize:) |
Use nth percentile of sample | Handles outliers |
.hybrid(sampleSize:, averageCharWidth:) |
Combine estimation with sampling | Robust |
.fixed(width:) |
Explicit pixel width | Instant |
Per-column overrides:
config.columnWidthModeProvider = { columnIndex in
switch columnIndex {
case 0: return .fixed(width: 80)
default: return nil // Use global mode
}
}Default Cell Configuration
config.defaultCellConfiguration = { cell, value, indexPath, isHighlighted in
cell.dataLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .regular)
cell.dataLabel.textColor = value.doubleValue < 0 ? .systemRed : .label
cell.backgroundColor = indexPath.section % 2 == 0 ? .systemBackground : .secondarySystemBackground
}Navigation Bar Search
var config = DataTableConfiguration()
config.searchBarPosition = .navigationBar
let table = SwiftDataTable(data: users, columns: columns, options: config)
table.attachSearchToNavigationBar(of: self)| Position | Behaviour |
|---|---|
.embedded |
Search bar within table (default) |
.navigationBar |
UISearchController in navigation item |
.hidden |
No search bar displayed |
Large-Scale Mode (100K+ rows)
config.rowHeightMode = .automatic(estimated: 44, prefetchWindow: 10)- Lazy Measurement — Rows start with estimated heights, measured on-demand
- Prefetch Window — Rows within window measured ahead for smooth scrolling
- O(viewport) Performance — Only visible rows measured
- Automatic Anchoring — Scroll position preserved when heights change
Scroll Anchoring
Data updates no longer cause visual jumps:
// User is viewing row 500
table.setData(newData, animatingDifferences: true)
// After update, user is still viewing row 500- Insertions above viewport — Content offset adjusts automatically
- Deletions above viewport — No jumping
- Height changes — Visual position preserved
- Anchor fallback — If anchor row deleted, nearest row becomes anchor
Breaking Changes
- iOS 17+ minimum (was iOS 13)
- Swift 5.9+ required
- Removed
columnWidthStrategy/columnWidthStrategyProvider/useEstimatedColumnWidths— usecolumnWidthMode DataTableCustomCellProvidernow uses dynamic reuse identifiers (reuseIdentifierFor/sizingCellFor)
Deprecated
| Deprecated | Replacement |
|---|---|
SwiftDataTableDataSource protocol |
Direct data with setData() |
reload() |
setData(_:animatingDifferences:) |
largeScale(estimatedHeight:) |
automatic(estimated:prefetchWindow:) |
| Delegate highlight colour methods | defaultCellConfiguration |
Bug Fixes
- Header width calculation now includes sort indicator space
- Search bar properly hides when disabled
- Fixed redundant String copy in
.stringRepresentation
Requirements
- iOS 17.0+
- Swift 5.9+
- Xcode 15.0+