Skip to content

Commit 097cc3d

Browse files
pavankatariaclaude
andcommitted
Add defaultCellConfiguration for easy cell styling
- Add DefaultCellConfiguration typealias and property to DataTableConfiguration - Make DataCell.dataLabel public for user access - Add prepareForReuse to DataCell to reset styling on cell reuse - Deprecate delegate colour methods (highlightedColorForRowIndex, unhighlightedColorForRowIndex) - Add comprehensive tests for the new API - Add Cell Styling demo with 5 interactive examples - Update documentation with DefaultCellConfiguration guide - Update README, Styling, CustomCells, and migration docs Resolves #39 (change font) and #17 (change cell colour) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 263a7cd commit 097cc3d

File tree

14 files changed

+1153
-37
lines changed

14 files changed

+1153
-37
lines changed

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)