Skip to content

Commit 263a7cd

Browse files
committed
Simplify typed API and update documentation
- Make data parameter optional in typed init - Deprecate array-based initialiser - Update all docs to use typed API with explanations
1 parent a91a152 commit 263a7cd

20 files changed

+401
-234
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

README.md

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,54 +82,42 @@ Display grid-like data with sorting, searching, and smooth animations - all in j
8282
```swift
8383
import SwiftDataTables
8484

85-
// Simple array-based table
86-
let data = [
87-
["Alice", "Engineer", "London"],
88-
["Bob", "Designer", "Paris"],
89-
["Carol", "Manager", "Berlin"]
90-
]
91-
let dataTable = SwiftDataTable(
92-
data: data,
93-
headerTitles: ["Name", "Role", "City"]
94-
)
95-
view.addSubview(dataTable)
96-
```
97-
98-
Or with type-safe columns:
99-
100-
```swift
85+
// Identifiable enables row tracking for animated updates
10186
struct Employee: Identifiable {
10287
let id: String
10388
let name: String
10489
let role: String
10590
let salary: Int
10691
}
10792

93+
// Key paths provide compile-time safety - typos caught at build time
10894
let columns: [DataTableColumn<Employee>] = [
10995
.init("Name", \.name),
11096
.init("Role", \.role),
111-
.init("Salary") { "£\($0.salary)" }
97+
.init("Salary") { "£\($0.salary)" } // Closures for formatted values
11298
]
11399

114-
let dataTable = SwiftDataTable(data: employees, columns: columns)
100+
let dataTable = SwiftDataTable(columns: columns)
101+
view.addSubview(dataTable)
102+
103+
// Rows animate in - scroll position preserved
104+
dataTable.setData(employees, animatingDifferences: true)
115105
```
116106

117-
Update data with animated diffing:
107+
Update data with animated diffing - SwiftDataTables calculates exactly what changed:
118108

119109
```swift
120-
// Load initial data
121-
var employees: [Employee] = []
122-
dataTable.setData(employees)
123-
124-
// Fetch and update
110+
// Only changed rows animate - others stay in place
125111
employees = await api.fetchEmployees()
126112
dataTable.setData(employees, animatingDifferences: true)
127113

128-
// Append new items
114+
// New row slides in at the end
129115
employees.append(newEmployee)
130116
dataTable.setData(employees, animatingDifferences: true)
131117
```
132118

119+
For dynamic data (CSV, JSON, queries), see [Working with Data](https://pavankataria.github.io/SwiftDataTables/documentation/swiftdatatables/workingwithdata).
120+
133121
## Install
134122

135123
### Swift Package Manager
@@ -305,7 +293,7 @@ SwiftDataTables supports native iOS navigation bar search via UISearchController
305293
override func viewDidLoad() {
306294
super.viewDidLoad()
307295

308-
let dataTable = SwiftDataTable(data: myData, headerTitles: headers)
296+
let dataTable = SwiftDataTable(columns: columns)
309297
view.addSubview(dataTable)
310298

311299
// One line to enable navigation bar search
@@ -331,7 +319,7 @@ navigationItem.searchController = searchController
331319

332320
## Data Source methods (Deprecated)
333321

334-
> **Note**: The `SwiftDataTableDataSource` protocol is deprecated in v0.9.0. Use the direct data pattern with `init(data:headerTitles:)` or the typed API with `init(data:columns:)` instead. See the [Quick Start](#quick-start) section above for examples.
322+
> **Note**: The `SwiftDataTableDataSource` protocol is deprecated in v0.9.0. Use the typed API with `init(columns:)` and `setData(_:animatingDifferences:)` instead. See the [Quick Start](#quick-start) section above for examples.
335323
336324
The deprecated protocol is shown below for reference:
337325

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/Classes/SwiftDataTable.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,25 @@ public class SwiftDataTable: UIView {
254254
self.registerObservers()
255255
}
256256

257+
/// Creates a data table with raw array data and header titles.
258+
///
259+
/// - Important: This initializer is deprecated. Use the type-safe column API instead:
260+
/// ```swift
261+
/// let columns: [DataTableColumn<YourModel>] = [
262+
/// .init("Name", \.name),
263+
/// .init("Age", \.age)
264+
/// ]
265+
/// let table = SwiftDataTable(columns: columns)
266+
/// table.setData(yourModels, animatingDifferences: true)
267+
/// ```
268+
///
269+
/// The type-safe API provides:
270+
/// - Automatic diffing with animated updates
271+
/// - Type safety via generics and KeyPaths
272+
/// - No manual array conversion
273+
///
274+
/// For dynamic data (CSV, JSON, database queries), see <doc:WorkingWithData>.
275+
@available(*, deprecated, message: "Use init(columns:) with DataTableColumn<T> for type-safe diffing. See documentation for migrating dynamic data.")
257276
public init(data: DataTableContent,
258277
headerTitles: [String],
259278
options: DataTableConfiguration = DataTableConfiguration(),
@@ -266,6 +285,11 @@ public class SwiftDataTable: UIView {
266285

267286

268287
}
288+
/// Creates a data table with string array data and header titles.
289+
///
290+
/// - Important: This initializer is deprecated. Use the type-safe column API instead.
291+
/// See ``init(data:headerTitles:options:frame:)-7kg3f`` for migration guidance.
292+
@available(*, deprecated, message: "Use init(columns:) with DataTableColumn<T> for type-safe diffing.")
269293
public convenience init(data: [[String]],
270294
headerTitles: [String],
271295
options: DataTableConfiguration = DataTableConfiguration(),

0 commit comments

Comments
 (0)