Releases: pavankataria/SwiftDataTables
0.9.0 - Self-Sizing Cells, Auto Layout & 928x Performance Boost
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+
Fixes archiving issues.
0.8.1
Summary:
- You will now automatically truly support right-to-left (RTL) language users.
Technical:
- Previously only the text within the data table would be flipped RTL for RTL languages but the content within the scrollview - such as columns - would remain in left-to-right. This release introduces the fix and ensures the layout direction is also along with the text giving a true RTL experience to RTL language users.
In practise
How do I opt in and ensure my users can benefit from this feature?
The package automatically enables this feature for you.
How can I manage this feature myself?
You can choose to opt out or opt in for certain data tables in two ways:
Via Delegate method
Simply adopt the SwiftDataTable's delegate method with the following signature:
func shouldSupportRightToLeftInterfaceDirection() -> Bool {
// and return true or false here. True will allow the data table to detect if a RTL language is being used and will flip the entire layout appropriately and display content correctly.
// false will leave the layout direction and only flip the text which would happen regardless anyway without this feature.
return true
}Via Configuration
var configuration = DataTableConfiguration()
// The default is true
configuration.shouldSupportRightToLeftInterfaceDirection: Bool = false // set to false incase you don't want to use this featureFixed Columns are here! Freeze those columns!
Summary:
- You can now have fixed columns on both left and right hand sides! It's as easy as either specifying the
fixedColumnsproperty in the configuration object, or by implementing thefixedColumnsdelegate method.
Technical:
- Due to objective-c delegate requirements the type you return in the
fixedColumnsdelegate method is a class instead of Swift's struct type. - Tests have been added to ensure the new
DataTableFixedColumnTypeinitialises as expected. - The
leftColumnandrightColumnarguments in the initialiser are one-index based. That is they start at 1. This is to create a natural declaration of the fixed columns. For example,DataTableFixedColumnType.init(leftColumns: 2, rightColumns: 2)would fix the first 2 columns (from the left) and the last 2 columns (from the right). - As usual the delegate method will take priority. So the fixedColumns object will first be fetched from the delegate method and if it's not implemented it will fallback on the configuration object. So if you want the fixedColumn configuration value to be read then you need to make sure to omit the fixedColumns delegate method.
In practise
How to use the new Fixed columns feature!
Here's how you define which columns you want frozen, by using the all new DataTableFixedColumnType class, and using any of the initialisers like so
// Example 1 - freeze from the left
DataTableFixedColumnType(leftColumns: 1) // this will freeze the n number of columns from the left of the table. In this case column number 1 - the first columns. This is a one-index based system
// Example 2 .- freeze from the right
DataTableFixedColumnType(rightColumns: 1) // this will freeze n number of columns from the right of the table. In this case the last column.
// Example 3 - multiple columns
DataTableFixedColumnType(leftColumns: 2, rightColumns1) // You can specify multiple columns to be frozen on both sides. In this case the first 2 columns and the last column.You can implement fixed columns in your data table in two ways:
Via Delegate method
Simply adopt the SwiftDataTable's delegate method with the following signature:
@objc optional func fixedColumns(for dataTable: SwiftDataTable) -> DataTableFixedColumnType {
// and return the object here
return .init(leftColumn: 2) // freeze the first two columns
}Via Configuration
var configuration = DataTableConfiguration()
configuration.fixedColumns = .init(leftColumns: 2, rightColumns: 1) // freeze both the first two columns, and the last columnSwift 5, beginning of Tests, fixes, and improvements!
Summary:
- Library upgraded to Swift 5
- Introduction of tests to reduce regression and make the library more bullet proof.
- Sorting arrow colour can be customised now in the configuration object.
- Fix: Using a specific initialiser would ignore DataTableConfiguration leading to custom configurations not being used.
Technical:
- The swift language has been upgraded to swift 5
- A test target has been added, with an initial tests to ensure initialisation with custom configuration works for a specific initialiser that would previously not initialise with the configuration object.
- The
SwiftDataTableConfigurationandDataTableColumnOrdermodels now conforms to Equatable - The sorting arrow icons now render as template mode enabling a custom colour.
Fixes a crash when datasource is empty and makes datasource rows public
Summary:
- Fixes crash when datasource is empty
- Demo project updated to show an empty state
rowsproperty is now available allowing you to access the current datasource as it is - maintained by SwiftDataTable.- There's a new method which allows you to fetch the data value type for a given index path - see technical information for more details.
Technical:
- There is a new method added allowing you to fetch a data value type for a given index path:
public func data(for indexPath: IndexPath) -> DataTableValueTypeThis method can be used as a convenience method or you can use the rows property to access the multi-dimensional array manually.
Removal of cocoapods from Demo project
Cocoapod dependencies removed from the demo project.
Swift 4.2 support with did select and deselect delegate methods
Going forwards the library will support Swift 4.2 support.
There's also a new did select and deselect delegate method:
/// Fired when a cell is selected.
///
/// - Parameters:
/// - dataTable: SwiftDataTable
/// - indexPath: the index path of the row selected
@objc optional func didSelectItem(_ dataTable: SwiftDataTable, indexPath: IndexPath)
/// Fired when a cell has been deselected
///
/// - Parameters:
/// - dataTable: SwiftDataTable
/// - indexPath: the index path of the row deselected
@objc optional func didDeselectItem(_ dataTable: SwiftDataTable, indexPath: IndexPath)Delegate for customisation!
Delegate customisation!
Now all these delegate methods are available for customising your swift data table.
All these methods are optionals and default values will be provided. This is not a breaking change.
These same properties are also available in the DataTableConfiguration class allowing you to provide more defaults without requiring a delegate. The delegate is useful if you want to customise dynamically. I.e. specifying different row heights for different row indexes.
/// An optional delegate for further customisation. Default values will be used retrieved from the SwiftDataTableConfiguration file. This will can be overridden and passed into the SwiftDataTable constructor incase you wish not to use the delegate.
@objc public protocol SwiftDataTableDelegate: class {
/// Specify custom heights for specific rows. A row height of 0 is valid and will be used.
///
/// - Parameters:
/// - dataTable: SwiftDataTable
/// - index: the index of the row to specify a custom height for.
/// - Returns: the desired height for the given row index
@objc optional func dataTable(_ dataTable: SwiftDataTable, heightForRowAt index: Int) -> CGFloat
/// Specify custom widths for columns. This method once implemented overrides the automatic width calculation for remaining columns and therefor widths for all columns must be given. This behaviour may change so that custom widths on a single column basis can be given with the automatic width calculation behaviour applied for the remaining columns.
/// - Parameters:
/// - dataTable: SwiftDataTable
/// - index: the index of the column to specify the width for
/// - Returns: the desired width for the given column index
@objc optional func dataTable(_ dataTable: SwiftDataTable, widthForColumnAt index: Int) -> CGFloat
/// Column Width scaling. If set to true and the column's total width is smaller than the content size then the width of each column will be scaled proprtionately to fill the frame of the table. Otherwise an automatic calculated width size will be used by processing the data within each column.
/// Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether you wish to scale to fill the frame of the table
@objc optional func shouldContentWidthScaleToFillFrame(in dataTable: SwiftDataTable) -> Bool
/// Section Header floating. If set to true headers can float and remain in view during scroll. Otherwise if set to false the header will be fixed at the top and scroll off view along with the content.
/// Defaults to true
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether you wish to float section header views.
@objc optional func shouldSectionHeadersFloat(in dataTable: SwiftDataTable) -> Bool
/// Section Footer floating. If set to true footers can float and remain in view during scroll. Otherwise if set to false the footer will be fixed at the top and scroll off view along with the content.
/// Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether you wish to float section footer views.
@objc optional func shouldSectionFootersFloat(in dataTable: SwiftDataTable) -> Bool
/// Search View floating. If set to true the search view can float and remain in view during scroll. Otherwise if set to false the search view will be fixed at the top and scroll off view along with the content.
// Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether you wish to float section footer views.
@objc optional func shouldSearchHeaderFloat(in dataTable: SwiftDataTable) -> Bool
/// Disable search view. Hide search view. Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether or not the search should be hidden
@objc optional func shouldShowSearchSection(in dataTable: SwiftDataTable) -> Bool
/// The height of the section footer. Defaults to 44.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: the height of the section footer
@objc optional func heightForSectionFooter(in dataTable: SwiftDataTable) -> CGFloat
/// The height of the section header. Defaults to 44.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: the height of the section header
@objc optional func heightForSectionHeader(in dataTable: SwiftDataTable) -> CGFloat
/// The height of the search view. Defaults to 44.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: the height of the search view
@objc optional func heightForSearchView(in dataTable: SwiftDataTable) -> CGFloat
/// Height of the inter row spacing. Defaults to 1.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: the height of the inter row spacing
@objc optional func heightOfInterRowSpacing(in dataTable: SwiftDataTable) -> CGFloat
/// Control the display of the vertical scroll bar. Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether or not the vertical scroll bars should be shown.
@objc optional func shouldShowVerticalScrollBars(in dataTable: SwiftDataTable) -> Bool
/// Control the display of the horizontal scroll bar. Defaults to true.
///
/// - Parameter dataTable: SwiftDataTable
/// - Returns: whether or not the horizontal scroll bars should be shown.
@objc optional func shouldShowHorizontalScrollBars(in dataTable: SwiftDataTable) -> Bool
}Swift 4, Speed x 300, and shouldShowFooter feature!
Summary:
- Library upgraded to Swift 4,
- Speed optimisations to return layout attributes for a given rect by a factor of 300 during scrolling!
- A requested feature to enable the hiding of the footer labels
- Example project updated to reflect the new visual footer hiding configuration.
Technical:
- Example project storyboard removed in favour of a programmatic menu view controller. Makes it easier to demonstrate variations of the
DataTableConfigurationobject using aGenericDataTableViewController. - The swift language has been upgraded to swift 4 and runs on xcode 9.
- The
DataTableConfigurationobject has a new property calledshouldShowFooterwith a default value oftrue. This property allows the user to override whether footers should be shown or not. - Refactors the
layoutAttributesForRectmethod to use a binary search instead of filtering through all cached layouattributes. This sees a speed factory of 300 during scroll. - Aims to increase the prepare layout method in the
SwiftDataTableLayoutclass by preventing unnecessary delegate calls for column widths.