Skip to content

Commit 3d51993

Browse files
pavankatariaclaude
andcommitted
Simplify Typed API: make data optional, remove columns from setData
API improvements: - Make `data` parameter optional in typed init (defaults to []) - Remove `columns:` parameter from setData() - columns stored at init - Add setColumns() method for changing columns after initialization This enables cleaner async loading patterns: let table = SwiftDataTable(columns: columns) table.setData(users, animatingDifferences: true) Updated all DocC documentation and added tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a91a152 commit 3d51993

File tree

10 files changed

+150
-91
lines changed

10 files changed

+150
-91
lines changed

Example/SwiftDataTablesTests/SwiftDataTableIncrementalUpdatesTests.swift

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -479,20 +479,14 @@ final class SwiftDataTableContentChangeTests: XCTestCase {
479479
.init("Price") { .double($0.price) }
480480
]
481481

482-
let initialContent: DataTableContent = stocks.map {
483-
[.string($0.symbol), .double($0.price)]
484-
}
485-
let table = makeSpyTableInWindow(data: initialContent, headerTitles: ["Symbol", "Price"])
486-
487-
// Seed identifiers + typed context
488-
table.setData(stocks, columns: columns, animatingDifferences: false)
482+
let table = makeSpyTableInWindow(data: stocks, columns: columns)
489483
table.spyCollectionView.resetTracking()
490484

491485
// When: Update only one row
492486
stocks[1].price = 250
493487

494488
let expectation = XCTestExpectation(description: "Diff completes")
495-
table.setData(stocks, columns: columns, animatingDifferences: true) { _ in
489+
table.setData(stocks, animatingDifferences: true) { _ in
496490
expectation.fulfill()
497491
}
498492
wait(for: [expectation], timeout: 1.0)
@@ -513,18 +507,13 @@ final class SwiftDataTableContentChangeTests: XCTestCase {
513507
.init("Price") { .double($0.price) }
514508
]
515509

516-
let initialContent: DataTableContent = stocks.map {
517-
[.string($0.symbol), .double($0.price)]
518-
}
519-
let table = makeSpyTableInWindow(data: initialContent, headerTitles: ["Symbol", "Price"])
520-
521-
table.setData(stocks, columns: columns, animatingDifferences: false)
510+
let table = makeSpyTableInWindow(data: stocks, columns: columns)
522511
table.spyCollectionView.resetTracking()
523512

524513
stocks[0].price = 105
525514

526515
let expectation = XCTestExpectation(description: "Diff completes")
527-
table.setData(stocks, columns: columns, animatingDifferences: true) { _ in
516+
table.setData(stocks, animatingDifferences: true) { _ in
528517
expectation.fulfill()
529518
}
530519
wait(for: [expectation], timeout: 1.0)
@@ -533,6 +522,27 @@ final class SwiftDataTableContentChangeTests: XCTestCase {
533522
XCTAssertTrue(table.spyCollectionView.reloadedSections.isEmpty)
534523
}
535524

525+
func test_columnsOnlyInit_thenSetData_works() {
526+
// Given: Columns defined, no initial data
527+
let columns: [DataTableColumn<Stock>] = [
528+
.init("Symbol", \.symbol),
529+
.init("Price") { .double($0.price) }
530+
]
531+
532+
// When: Create table with columns only (no data)
533+
let table = makeSpyTableInWindow(data: [] as [Stock], columns: columns)
534+
XCTAssertEqual(table.numberOfRows(), 0)
535+
536+
// Then: setData populates the table
537+
let stocks = [
538+
Stock(id: "1", symbol: "AAPL", price: 100),
539+
Stock(id: "2", symbol: "GOOGL", price: 200)
540+
]
541+
table.setData(stocks, animatingDifferences: false)
542+
543+
XCTAssertEqual(table.numberOfRows(), 2)
544+
}
545+
536546
func test_rowContentEqual_returnsTrue_forIdenticalRows() {
537547
// Given: Two identical view model rows
538548
let row1 = [
@@ -748,6 +758,22 @@ final class SwiftDataTableContentChangeTests: XCTestCase {
748758

749759
return table
750760
}
761+
762+
private func makeSpyTableInWindow<T: Identifiable>(data: [T], columns: [DataTableColumn<T>]) -> SwiftDataTableCollectionViewSpy {
763+
var config = DataTableConfiguration()
764+
config.shouldShowSearchSection = false
765+
config.rowHeightMode = .fixed(44)
766+
767+
let table = SwiftDataTableCollectionViewSpy(data: data, columns: columns, options: config)
768+
table.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
769+
770+
let window = UIWindow(frame: table.frame)
771+
window.addSubview(table)
772+
window.makeKeyAndVisible()
773+
table.layoutIfNeeded()
774+
775+
return table
776+
}
751777
}
752778

753779
// MARK: - Spy Classes
@@ -794,7 +820,22 @@ private final class SwiftDataTableCollectionViewSpy: SwiftDataTable {
794820
collectionViewLayout: UICollectionViewFlowLayout()
795821
)
796822
super.init(data: data, headerTitles: headerTitles, options: options)
823+
setupSpyCollectionView()
824+
}
825+
826+
convenience init<T: Identifiable>(data: [T], columns: [DataTableColumn<T>], options: DataTableConfiguration) {
827+
let headerTitles = columns.map { $0.header }
828+
let content: DataTableContent = data.map { item in
829+
columns.map { column in
830+
column.extract?(item) ?? .string("")
831+
}
832+
}
833+
self.init(data: content, headerTitles: headerTitles, options: options)
834+
storeTypedContext(data: data, columns: columns)
835+
seedRowIdentifiers(data.map { "\($0.id)" })
836+
}
797837

838+
private func setupSpyCollectionView() {
798839
spyCollectionView.dataSource = self
799840
spyCollectionView.delegate = self
800841
spyCollectionView.backgroundColor = .clear

SwiftDataTables/Classes/Extensions/SwiftDataTable+TypedAPI.swift

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public extension SwiftDataTable {
6262
/// - options: Configuration options for the table.
6363
/// - frame: Initial frame for the view.
6464
convenience init<T: Identifiable>(
65-
data: [T],
65+
data: [T] = [],
6666
columns: [DataTableColumn<T>],
6767
options: DataTableConfiguration = DataTableConfiguration(),
6868
frame: CGRect = .zero
@@ -100,7 +100,6 @@ public extension SwiftDataTable {
100100
///
101101
/// - Parameters:
102102
/// - data: The new complete data set.
103-
/// - columns: Column definitions (uses stored columns if nil).
104103
/// - animatingDifferences: Whether to animate the changes.
105104
/// - completion: Called when the update completes.
106105
///
@@ -115,18 +114,12 @@ public extension SwiftDataTable {
115114
/// ```
116115
func setData<T: Identifiable>(
117116
_ data: [T],
118-
columns: [DataTableColumn<T>]? = nil,
119117
animatingDifferences: Bool = true,
120118
completion: ((Bool) -> Void)? = nil
121119
) {
122-
// Get column extractors
123-
let columnDefs: [DataTableColumn<T>]
124-
if let cols = columns {
125-
columnDefs = cols
126-
} else if let stored = getStoredColumns() as? [DataTableColumn<T>] {
127-
columnDefs = stored
128-
} else {
129-
assertionFailure("No columns provided and no stored columns found. Provide columns parameter.")
120+
// Get column extractors from stored context
121+
guard let columnDefs = getStoredColumns() as? [DataTableColumn<T>] else {
122+
assertionFailure("No stored columns found. Use init(columns:) to create the table first.")
130123
completion?(false)
131124
return
132125
}
@@ -185,23 +178,16 @@ public extension SwiftDataTable {
185178
///
186179
/// - Parameters:
187180
/// - data: The new complete data set.
188-
/// - columns: Column definitions (uses stored columns if nil).
189181
/// - animatingDifferences: Whether to animate the changes.
190182
/// - completion: Called when the update completes.
191183
func setData<T: DataTableDifferentiable>(
192184
_ data: [T],
193-
columns: [DataTableColumn<T>]? = nil,
194185
animatingDifferences: Bool = true,
195186
completion: ((Bool) -> Void)? = nil
196187
) {
197-
// Get column extractors
198-
let columnDefs: [DataTableColumn<T>]
199-
if let cols = columns {
200-
columnDefs = cols
201-
} else if let stored = getStoredColumns() as? [DataTableColumn<T>] {
202-
columnDefs = stored
203-
} else {
204-
assertionFailure("No columns provided and no stored columns found. Provide columns parameter.")
188+
// Get column extractors from stored context
189+
guard let columnDefs = getStoredColumns() as? [DataTableColumn<T>] else {
190+
assertionFailure("No stored columns found. Use init(columns:) to create the table first.")
205191
completion?(false)
206192
return
207193
}
@@ -266,12 +252,45 @@ public extension SwiftDataTable {
266252
func allModels<T>() -> [T]? {
267253
return getStoredData() as? [T]
268254
}
255+
256+
// MARK: - Column Updates
257+
258+
/// Updates the column definitions and reloads the table.
259+
///
260+
/// Use this when you need to change which columns are displayed or how
261+
/// values are extracted. The table reloads completely with the new columns.
262+
///
263+
/// - Parameters:
264+
/// - columns: New column definitions.
265+
/// - data: Optional new data. If nil, uses existing stored data.
266+
func setColumns<T: Identifiable>(_ columns: [DataTableColumn<T>], data: [T]? = nil) {
267+
let dataToUse = data ?? (getStoredData() as? [T]) ?? []
268+
269+
// Extract headers from columns
270+
let headerTitles = columns.map { $0.header }
271+
272+
// Convert typed data to DataTableContent
273+
let content: DataTableContent = dataToUse.map { item in
274+
columns.map { column in
275+
column.extract?(item) ?? .string("")
276+
}
277+
}
278+
279+
// Store new context
280+
storeTypedContext(data: dataToUse, columns: columns)
281+
282+
// Update data with new headers
283+
set(data: content, headerTitles: headerTitles)
284+
reload()
285+
}
269286
}
270287

271-
// MARK: - Private Storage
288+
// MARK: - Internal Storage (visible to tests via @testable import)
272289

273-
private extension SwiftDataTable {
290+
extension SwiftDataTable {
274291

292+
/// Stores typed data and column definitions for later retrieval.
293+
/// - Note: Internal access for testing. Not part of public API.
275294
func storeTypedContext<T>(data: [T], columns: [DataTableColumn<T>]) {
276295
typedData = data
277296
typedColumns = columns

SwiftDataTables/SwiftDataTables.docc/AdvancedPatterns.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class DashboardVC: UIViewController, SwiftDataTableDelegate {
6666
UIColor.systemGreen.withAlphaComponent(0.15)
6767
]
6868

69-
dataTable = SwiftDataTable(data: metrics, columns: columns, options: config)
69+
dataTable = SwiftDataTable(data: metrics, options: config)
7070
dataTable.delegate = self
7171
setupLayout()
7272
}
@@ -101,7 +101,7 @@ class LiveDataVC: UIViewController {
101101
config.shouldSearchHeaderFloat = true
102102
config.rowHeightMode = .fixed(36) // Compact rows
103103

104-
dataTable = SwiftDataTable(data: items, columns: columns, options: config)
104+
dataTable = SwiftDataTable(data: items, options: config)
105105
setupLayout()
106106

107107
// Start polling
@@ -120,7 +120,7 @@ class LiveDataVC: UIViewController {
120120
await MainActor.run {
121121
items = newItems
122122
// Animated update preserves scroll position
123-
dataTable.setData(items, columns: columns, animatingDifferences: true)
123+
dataTable.setData(items, animatingDifferences: true)
124124
}
125125
}
126126
}
@@ -200,7 +200,7 @@ class MasterVC: UIViewController, SwiftDataTableDelegate {
200200
var config = DataTableConfiguration()
201201
config.shouldShowFooter = false
202202

203-
dataTable = SwiftDataTable(data: items, columns: columns, options: config)
203+
dataTable = SwiftDataTable(data: items, options: config)
204204
dataTable.delegate = self
205205
setupLayout()
206206
}
@@ -251,7 +251,7 @@ class FilteredTableVC: UIViewController {
251251
super.viewDidLoad()
252252
navigationItem.titleView = segmentedControl
253253

254-
dataTable = SwiftDataTable(data: allItems, columns: columns)
254+
dataTable = SwiftDataTable(data: allItems)
255255
setupLayout()
256256
}
257257

@@ -270,7 +270,7 @@ class FilteredTableVC: UIViewController {
270270
filtered = allItems.filter { $0.isCompleted }
271271
}
272272

273-
dataTable.setData(filtered, columns: columns, animatingDifferences: true)
273+
dataTable.setData(filtered, animatingDifferences: true)
274274
}
275275
}
276276
```
@@ -302,7 +302,7 @@ class LargeDatasetVC: UIViewController {
302302
)
303303
config.lockColumnWidthsAfterFirstLayout = true
304304

305-
dataTable = SwiftDataTable(data: [], columns: columns, options: config)
305+
dataTable = SwiftDataTable(data: [], options: config)
306306
setupLayout()
307307

308308
// Show loading state
@@ -314,7 +314,7 @@ class LargeDatasetVC: UIViewController {
314314
await MainActor.run {
315315
self.hideLoadingIndicator()
316316
self.items = loaded
317-
self.dataTable.setData(loaded, columns: self.columns, animatingDifferences: false)
317+
self.dataTable.setData(loaded, animatingDifferences: false)
318318
}
319319
}
320320
}
@@ -366,7 +366,7 @@ class EmployeeListVC: UIViewController, SwiftDataTableDelegate {
366366
index == 2 ? .fixed(width: 120) : nil
367367
}
368368

369-
dataTable = SwiftDataTable(data: employees, columns: columns, options: config)
369+
dataTable = SwiftDataTable(data: employees, options: config)
370370
dataTable.delegate = self
371371
setupLayout()
372372
}
@@ -400,7 +400,7 @@ class EmployeeListVC: UIViewController, SwiftDataTableDelegate {
400400

401401
func deleteEmployee(at index: Int) {
402402
employees.remove(at: index)
403-
dataTable.setData(employees, columns: columns, animatingDifferences: true)
403+
dataTable.setData(employees, animatingDifferences: true)
404404
}
405405
}
406406
```

0 commit comments

Comments
 (0)