Skip to content

Commit 6eb31cc

Browse files
committed
Add defaultCellConfiguration for easy cell styling
Customise default cell fonts, colours, and styling without creating custom cell classes. Includes demo, tests, and documentation. Resolves #39 and #17.
1 parent 263a7cd commit 6eb31cc

File tree

15 files changed

+1178
-39
lines changed

15 files changed

+1178
-39
lines changed

CHANGELOG.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10-
## [Unreleased] - v0.9.0: Self-Sizing Cells & Auto Layout
10+
## [0.9.0] - 2026-01-28: Self-Sizing Cells, Auto Layout & Default Cell Configuration
1111

1212
### Summary
1313
This release adds first-class support for **Auto Layout-driven cells** with automatic row heights and text wrapping. Use custom `UICollectionViewCell` subclasses with full constraint-based sizing to build rich, dynamic table layouts.
1414

15-
Also includes: new column width strategy API, major performance optimizations for large datasets, and bug fixes.
15+
Also includes: **default cell configuration** for easy styling without custom cells, new column width strategy API, major performance optimizations for large datasets, and bug fixes.
1616

1717
### Column Width Mode API
1818
- Added `DataTableColumnWidthMode` with explicit text-based and Auto Layout-based sizing.
@@ -114,6 +114,19 @@ config.columnWidthMode = .fitContentText(strategy: .maxMeasured) // Use font mea
114114

115115
### Added
116116

117+
- **`DataTableConfiguration.defaultCellConfiguration`** (`DefaultCellConfiguration`)
118+
- Customise the default `DataCell` appearance without creating custom cell classes
119+
- Set font, text colour, background colour, alignment, and more per-cell
120+
- Callback receives `(cell, value, indexPath, isHighlighted)` for conditional styling
121+
- Perfect for alternating row colours, highlighting negative values, per-column fonts
122+
- See `DefaultCellConfiguration.md` documentation for examples
123+
124+
- **`DataCell.dataLabel` now public**
125+
- Access the label directly in `defaultCellConfiguration` to customise font, colour, alignment
126+
127+
- **`DataCell.prepareForReuse()`**
128+
- Resets label styling on cell reuse to prevent stale styles from persisting
129+
117130
- **`DataTableConfiguration.columnWidthMode`** (`DataTableColumnWidthMode`)
118131
- Explicitly selects text measurement or Auto Layout measurement for column widths
119132
- Supports per-column overrides via `columnWidthModeProvider`
@@ -127,6 +140,16 @@ config.columnWidthMode = .fitContentText(strategy: .maxMeasured) // Use font mea
127140

128141
---
129142

143+
### Deprecated
144+
145+
- **`SwiftDataTableDelegate.dataTable(_:highlightedColorForRowIndex:)`**
146+
- Use `DataTableConfiguration.defaultCellConfiguration` instead
147+
148+
- **`SwiftDataTableDelegate.dataTable(_:unhighlightedColorForRowIndex:)`**
149+
- Use `DataTableConfiguration.defaultCellConfiguration` instead
150+
151+
---
152+
130153
### Fixed
131154

132155
- **Header column width calculation bug (unit mismatch)**: When using estimated widths, header titles were compared incorrectly against data widths. "Name" header (4 chars) was compared as `4` against data values measured in points (~35). Now both are in the same unit.

Example/DemoSwiftDataTables/App/MenuTableViewController.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,12 @@ class MenuViewController: UITableViewController {
113113
],
114114
// Section 6: Visual Styling
115115
[
116+
MenuItem(title: "Cell Styling"),
116117
MenuItem(title: "Sort Arrow Styling"),
117118
MenuItem(
118-
title: "Alternating Row Colours",
119+
title: "Alternating Row Colours (Simple)",
119120
config: configurationAlternatingColours(),
120-
description: "Custom rainbow alternating row colours. Set highlightedAlternatingRowColors and unhighlightedAlternatingRowColors."
121+
description: "Simple alternating row colours using highlightedAlternatingRowColors and unhighlightedAlternatingRowColors arrays. For more control (fonts, conditional styling), use defaultCellConfiguration instead - see Cell Styling demo."
121122
),
122123
],
123124
// Section 7: Performance
@@ -273,8 +274,10 @@ extension MenuViewController {
273274
private func handleVisualStyling(row: Int) {
274275
switch row {
275276
case 0:
276-
show(SortArrowStylingDemoViewController(), sender: self)
277+
show(CellStylingDemoViewController(), sender: self)
277278
case 1:
279+
show(SortArrowStylingDemoViewController(), sender: self)
280+
case 2:
278281
let menuItem = menuItems[Section.visualStyling.rawValue][row]
279282
if let config = menuItem.config {
280283
let instance = GenericDataTableViewController(
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// CellStylingDemoViewController+Explanation.swift
3+
// SwiftDataTables
4+
//
5+
// Created by Pavan Kataria on 28/01/2026.
6+
// Copyright © 2016-2026 Pavan Kataria. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
extension CellStylingDemoViewController {
12+
13+
struct ExplanationControls {
14+
let view: DemoExplanationView
15+
let styleButtons: [UIButton]
16+
}
17+
18+
func makeExplanationControls(styleOptions: [String]) -> ExplanationControls {
19+
let styleButtons: [UIButton] = styleOptions.enumerated().map { index, title in
20+
let button = UIButton(type: .system)
21+
button.setTitle(title, for: .normal)
22+
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
23+
button.titleLabel?.numberOfLines = 2
24+
button.titleLabel?.textAlignment = .center
25+
button.tag = index
26+
button.addTarget(self, action: #selector(styleButtonTapped(_:)), for: .touchUpInside)
27+
button.layer.cornerRadius = 8
28+
button.layer.borderWidth = 1
29+
button.layer.borderColor = UIColor.systemBlue.cgColor
30+
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
31+
return button
32+
}
33+
34+
let row1 = UIStackView(arrangedSubviews: Array(styleButtons[0..<3]))
35+
row1.axis = .horizontal
36+
row1.spacing = 8
37+
row1.distribution = .fillEqually
38+
39+
let row2 = UIStackView(arrangedSubviews: Array(styleButtons[3..<5]))
40+
row2.axis = .horizontal
41+
row2.spacing = 8
42+
row2.distribution = .fillEqually
43+
44+
let buttonStack = UIStackView(arrangedSubviews: [row1, row2])
45+
buttonStack.axis = .vertical
46+
buttonStack.spacing = 8
47+
48+
let codeLabel = UILabel()
49+
codeLabel.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
50+
codeLabel.textColor = .secondaryLabel
51+
codeLabel.numberOfLines = 0
52+
codeLabel.text = """
53+
config.defaultCellConfiguration = { cell, value, indexPath, isHighlighted in
54+
cell.dataLabel.font = .custom
55+
cell.dataLabel.textColor = .conditional
56+
cell.backgroundColor = .alternating
57+
}
58+
"""
59+
60+
let explanationView = DemoExplanationView(
61+
description: """
62+
Customise default cells without creating custom cell classes. \
63+
Use defaultCellConfiguration to set font, text colour, background, \
64+
and more based on value, position, or highlight state.
65+
66+
This replaces the deprecated delegate methods \
67+
dataTable(_:highlightedColorForRowIndex:) and \
68+
dataTable(_:unhighlightedColorForRowIndex:).
69+
""",
70+
controls: [buttonStack, codeLabel]
71+
)
72+
73+
return ExplanationControls(
74+
view: explanationView,
75+
styleButtons: styleButtons
76+
)
77+
}
78+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
//
2+
// CellStylingDemoViewController.swift
3+
// SwiftDataTables
4+
//
5+
// Created by Pavan Kataria on 28/01/2026.
6+
// Copyright © 2016-2026 Pavan Kataria. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import SwiftDataTables
11+
12+
final class CellStylingDemoViewController: UIViewController {
13+
14+
// MARK: - Data
15+
16+
let sampleData = samplePeople()
17+
18+
let columns: [DataTableColumn<SamplePerson>] = [
19+
.init("ID", \.id),
20+
.init("Name", \.name),
21+
.init("Email", \.email),
22+
.init("Number", \.phone),
23+
.init("City", \.city),
24+
.init("Balance", \.balance)
25+
]
26+
27+
// MARK: - State
28+
29+
private var selectedStyleIndex: Int = 0
30+
31+
let styleOptions: [String] = [
32+
"Custom Font",
33+
"Negative Values Red",
34+
"Per-Column Styling",
35+
"Alternating Colours",
36+
"Combined Styling"
37+
]
38+
39+
// MARK: - UI
40+
41+
private var controls: ExplanationControls!
42+
private var dataTable: SwiftDataTable?
43+
44+
// MARK: - Lifecycle
45+
46+
override func viewDidLoad() {
47+
super.viewDidLoad()
48+
title = "Cell Styling"
49+
view.backgroundColor = .systemBackground
50+
51+
controls = makeExplanationControls(styleOptions: styleOptions)
52+
installExplanation(controls.view)
53+
54+
updateButtonAppearance()
55+
updateUI()
56+
rebuildTable()
57+
}
58+
59+
// MARK: - Actions
60+
61+
@objc func styleButtonTapped(_ sender: UIButton) {
62+
selectedStyleIndex = sender.tag
63+
updateButtonAppearance()
64+
updateUI()
65+
rebuildTable()
66+
}
67+
68+
// MARK: - UI Updates
69+
70+
private func updateButtonAppearance() {
71+
for (index, button) in controls.styleButtons.enumerated() {
72+
let isSelected = index == selectedStyleIndex
73+
button.backgroundColor = isSelected ? .systemBlue : .clear
74+
button.setTitleColor(isSelected ? .white : .systemBlue, for: .normal)
75+
}
76+
}
77+
78+
private func updateUI() {
79+
let descriptions = [
80+
"Custom font applied to all cells using Avenir-Medium.",
81+
"Balance column shows negative values in red, positive in green.",
82+
"ID column uses monospaced digits, City column is centered.",
83+
"Rainbow row colours cycling through 7 colours, with highlight for sorted column.",
84+
"All styling combined: font, conditional colours, and row striping."
85+
]
86+
controls.view.updateSummary(descriptions[selectedStyleIndex])
87+
}
88+
89+
private func rebuildTable() {
90+
dataTable?.removeFromSuperview()
91+
92+
var config = DataTableConfiguration()
93+
config.defaultOrdering = DataTableColumnOrder(index: 5, order: .descending) // Sort by Balance
94+
95+
// Apply the selected styling
96+
switch selectedStyleIndex {
97+
case 0:
98+
config.defaultCellConfiguration = customFontConfiguration()
99+
case 1:
100+
config.defaultCellConfiguration = negativeValuesConfiguration()
101+
case 2:
102+
config.defaultCellConfiguration = perColumnConfiguration()
103+
case 3:
104+
config.defaultCellConfiguration = alternatingColoursConfiguration()
105+
case 4:
106+
config.defaultCellConfiguration = combinedConfiguration()
107+
default:
108+
break
109+
}
110+
111+
let table = SwiftDataTable(data: sampleData, columns: columns, options: config)
112+
table.backgroundColor = UIColor(red: 235/255, green: 235/255, blue: 235/255, alpha: 1)
113+
114+
addDataTable(table, below: controls.view)
115+
dataTable = table
116+
}
117+
118+
// MARK: - Cell Configuration Styles
119+
120+
private func customFontConfiguration() -> DefaultCellConfiguration {
121+
return { cell, _, _, _ in
122+
cell.dataLabel.font = UIFont(name: "Avenir-Medium", size: 14)
123+
}
124+
}
125+
126+
private func negativeValuesConfiguration() -> DefaultCellConfiguration {
127+
return { cell, value, indexPath, _ in
128+
// Only apply colour logic to Balance column (index 5)
129+
if indexPath.section == 5 {
130+
let balance = value.stringRepresentation
131+
.replacingOccurrences(of: "£", with: "")
132+
.replacingOccurrences(of: ",", with: "")
133+
if let number = Double(balance) {
134+
if number < 0 {
135+
cell.dataLabel.textColor = .systemRed
136+
cell.dataLabel.font = .boldSystemFont(ofSize: 14)
137+
} else {
138+
cell.dataLabel.textColor = .systemGreen
139+
cell.dataLabel.font = .systemFont(ofSize: 14)
140+
}
141+
}
142+
} else {
143+
cell.dataLabel.textColor = .label
144+
cell.dataLabel.font = .systemFont(ofSize: 14)
145+
}
146+
}
147+
}
148+
149+
private func perColumnConfiguration() -> DefaultCellConfiguration {
150+
return { cell, _, indexPath, _ in
151+
switch indexPath.section {
152+
case 0: // ID column - monospaced
153+
cell.dataLabel.font = .monospacedDigitSystemFont(ofSize: 13, weight: .regular)
154+
cell.dataLabel.textColor = .secondaryLabel
155+
case 4: // City column - centered
156+
cell.dataLabel.font = .systemFont(ofSize: 14, weight: .medium)
157+
cell.dataLabel.textAlignment = .center
158+
case 5: // Balance column - right aligned
159+
cell.dataLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .semibold)
160+
cell.dataLabel.textAlignment = .right
161+
default:
162+
cell.dataLabel.font = .systemFont(ofSize: 14)
163+
cell.dataLabel.textAlignment = .natural
164+
cell.dataLabel.textColor = .label
165+
}
166+
}
167+
}
168+
169+
private func alternatingColoursConfiguration() -> DefaultCellConfiguration {
170+
return { cell, _, indexPath, isHighlighted in
171+
// Rainbow colours like the classic demo
172+
let highlightedColours: [UIColor] = [
173+
UIColor(red: 1, green: 0.7, blue: 0.7, alpha: 1),
174+
UIColor(red: 1, green: 0.7, blue: 0.5, alpha: 1),
175+
UIColor(red: 1, green: 1, blue: 0.5, alpha: 1),
176+
UIColor(red: 0.5, green: 1, blue: 0.5, alpha: 1),
177+
UIColor(red: 0.5, green: 0.7, blue: 1, alpha: 1),
178+
UIColor(red: 0.5, green: 0.5, blue: 1, alpha: 1),
179+
UIColor(red: 1, green: 0.5, blue: 0.5, alpha: 1)
180+
]
181+
let unhighlightedColours: [UIColor] = [
182+
UIColor(red: 1, green: 0.90, blue: 0.90, alpha: 1),
183+
UIColor(red: 1, green: 0.90, blue: 0.7, alpha: 1),
184+
UIColor(red: 1, green: 1, blue: 0.7, alpha: 1),
185+
UIColor(red: 0.7, green: 1, blue: 0.7, alpha: 1),
186+
UIColor(red: 0.7, green: 0.9, blue: 1, alpha: 1),
187+
UIColor(red: 0.7, green: 0.7, blue: 1, alpha: 1),
188+
UIColor(red: 1, green: 0.7, blue: 0.7, alpha: 1)
189+
]
190+
191+
let colours = isHighlighted ? highlightedColours : unhighlightedColours
192+
cell.backgroundColor = colours[indexPath.item % colours.count]
193+
}
194+
}
195+
196+
private func combinedConfiguration() -> DefaultCellConfiguration {
197+
return { cell, value, indexPath, isHighlighted in
198+
// 1. Alternating row colours
199+
let highlightedColours: [UIColor] = [
200+
.systemBlue.withAlphaComponent(0.08),
201+
.systemBlue.withAlphaComponent(0.12)
202+
]
203+
let unhighlightedColours: [UIColor] = [
204+
.systemBackground,
205+
.secondarySystemBackground
206+
]
207+
let colours = isHighlighted ? highlightedColours : unhighlightedColours
208+
cell.backgroundColor = colours[indexPath.item % colours.count]
209+
210+
// 2. Per-column styling
211+
switch indexPath.section {
212+
case 0: // ID
213+
cell.dataLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
214+
cell.dataLabel.textColor = .tertiaryLabel
215+
case 5: // Balance - conditional colouring
216+
cell.dataLabel.font = .monospacedDigitSystemFont(ofSize: 14, weight: .semibold)
217+
let balance = value.stringRepresentation
218+
.replacingOccurrences(of: "£", with: "")
219+
.replacingOccurrences(of: ",", with: "")
220+
if let number = Double(balance) {
221+
cell.dataLabel.textColor = number < 0 ? .systemRed : .systemGreen
222+
}
223+
default:
224+
cell.dataLabel.font = UIFont(name: "Avenir-Medium", size: 14)
225+
cell.dataLabel.textColor = .label
226+
}
227+
}
228+
}
229+
}

0 commit comments

Comments
 (0)