Skip to content

0.9.0 - Self-Sizing Cells, Auto Layout & 928x Performance Boost

Latest

Choose a tag to compare

@pavankataria pavankataria released this 29 Jan 20:22
· 8 commits to master since this release

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

  1. O(n) Layout Algorithm (was O(n²)) — Pre-calculate Y-offsets once, cache column widths and row heights
  2. Estimated Column Widths — Character count estimation (width = chars × 7pt) instead of font measurement
  3. Row Height Caching — Heights calculated once per data update

Type-Safe Columns API

Full guide →

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

Full guide →

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

Full guide →

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

Full guide →

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

Full guide →

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

Full guide →

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 — use columnWidthMode
  • DataTableCustomCellProvider now 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+

Full Documentation →

Vote for Features →