How to Handle Different Cell Types Within the Same Section in SectionKit
Use an enum to represent each cell type and switch on it inside itemSize(at:) and item(at:) to dequeue heterogeneous cells while conforming to SKCSectionProtocol and SKCSectionActionProtocol.
SectionKit decouples section logic from cell management through protocol-oriented design. To display multiple cell types within a single section, you leverage SKCSectionProtocol for lifecycle hooks and SKCSectionActionProtocol for registration and dequeuing helpers. This pattern keeps your code type-safe and declarative while allowing any mixture of headers, items, and footers inside one section.
The Architecture Behind Heterogeneous Sections
SectionKit separates responsibilities across two core protocols. SKCSectionProtocol, defined in Sources/SectionKit/CollectionBaseProtocol/SKCSectionProtocol.swift, binds a section to the manager and mandates methods for item count, sizing, and cell provision. SKCSectionActionProtocol, found in Sources/SectionUI/Sections/SKCSectionActionProtocol.swift, exposes convenience methods such as register(_:) for cell registration and dequeue(at:) for type-safe cell retrieval.
When you conform to both protocols, the manager automatically calls config(sectionView:) when the section is added to the collection view. This is where you register every cell class the section will display.
Step-by-Step Implementation
The following workflow demonstrates the pattern used in MixedCellsSectionTemplate.swift to mix headers, items, and footers in one section.
1. Define an Enum for Cell Types
Create an enum where each case carries the model required by its corresponding cell. This acts as the single source of truth for the section’s data source.
enum CellType {
case header(HeaderCell.Model)
case item(ItemCell.Model)
case footer(FooterCell.Model)
}
Store an array of this enum as a property on your section class. The itemCount property returns the count of this array.
2. Register Cell Classes in config(sectionView:)
Implement config(sectionView:) to register all cell classes with the collection view. The manager invokes this method automatically when the section is attached.
func config(sectionView: UICollectionView) {
register(HeaderCell.self)
register(ItemCell.self)
register(FooterCell.self)
}
The register(_:) method is provided by SKCSectionActionProtocol and handles the low-level register(_:forCellWithReuseIdentifier:) call on the underlying UICollectionView.
3. Calculate Sizes with itemSize(at:)
Switch on the enum to compute sizes dynamically based on the cell type at a specific index.
func itemSize(at row: Int) -> CGSize {
switch cellTypes[row] {
case .header(let model):
return HeaderCell.preferredSize(limit: safeSizeProvider.size, model: model)
case .item(let model):
return ItemCell.preferredSize(limit: safeSizeProvider.size, model: model)
case .footer(let model):
return FooterCell.preferredSize(limit: safeSizeProvider.size, model: model)
}
}
4. Dequeue and Configure Cells in item(at:)
Use the same switch pattern to dequeue the correct cell type and inject its model.
func item(at row: Int) -> UICollectionViewCell {
switch cellTypes[row] {
case .header(let model):
let cell = dequeue(at: row) as HeaderCell
cell.config(model)
return cell
case .item(let model):
let cell = dequeue(at: row) as ItemCell
cell.config(model)
return cell
case .footer(let model):
let cell = dequeue(at: row) as FooterCell
cell.config(model)
return cell
}
}
The dequeue(at:) helper returns a fully typed instance, eliminating the need for manual casting.
5. Handle Selection (Optional)
Implement item(selected:) to react to taps based on the cell type at the selected index.
func item(selected row: Int) {
if case .item(let model) = cellTypes[row] {
print("Item tapped: \(model.title)")
}
}
Complete Working Example
Below is the full implementation from .agent/skills/sectionui/examples/MixedCellsSectionTemplate.swift, demonstrating a section that accepts headers, items, and footers.
import UIKit
import SectionKit
import SectionUI
final class MixedCellsSectionTemplate: SKCSectionProtocol,
SKCSectionActionProtocol,
SKSafeSizeProviderProtocol {
enum CellType {
case header(HeaderCell.Model)
case item(ItemCell.Model)
case footer(FooterCell.Model)
}
var sectionInjection: SKCSectionInjection?
lazy var safeSizeProvider: SKSafeSizeProvider = defaultSafeSizeProvider
var cellTypes: [CellType] = []
var itemCount: Int { cellTypes.count }
init(cellTypes: [CellType] = []) {
self.cellTypes = cellTypes
}
func config(sectionView: UICollectionView) {
register(HeaderCell.self)
register(ItemCell.self)
register(FooterCell.self)
}
func itemSize(at row: Int) -> CGSize {
switch cellTypes[row] {
case .header(let model):
return HeaderCell.preferredSize(limit: safeSizeProvider.size, model: model)
case .item(let model):
return ItemCell.preferredSize(limit: safeSizeProvider.size, model: model)
case .footer(let model):
return FooterCell.preferredSize(limit: safeSizeProvider.size, model: model)
}
}
func item(at row: Int) -> UICollectionViewCell {
switch cellTypes[row] {
case .header(let model):
let cell = dequeue(at: row) as HeaderCell
cell.config(model)
return cell
case .item(let model):
let cell = dequeue(at: row) as ItemCell
cell.config(model)
return cell
case .footer(let model):
let cell = dequeue(at: row) as FooterCell
cell.config(model)
return cell
}
}
func item(selected row: Int) {
if case .item(let model) = cellTypes[row] {
print("Item tapped: \(model.title)")
}
}
}
Usage Example
Instantiate the section with an array of CellType values and pass it to your SKCManager.
let mixedSection = MixedCellsSectionTemplate(cellTypes: [
.header(.init(title: "Profile")),
.item(.init(title: "Name", subtitle: "Lin Hay")),
.item(.init(title: "Email", subtitle: "lin@example.com")),
.footer(.init(text: "2 items"))
])
manager.reload(mixedSection)
The manager handles the rest, calling the appropriate sizing and cell provision methods as the collection view scrolls.
Summary
- Conform to
SKCSectionProtocolto satisfy the manager’s data source and delegate requirements. - Adopt
SKCSectionActionProtocolto gain access toregister(_:)anddequeue(at:)helper methods. - Model heterogeneous data with an enum where each case contains the model for a specific cell type.
- Switch on the enum inside
itemSize(at:)anditem(at:)to branch logic for each cell class. - Register all cell classes inside
config(sectionView:)so the collection view can reuse cells correctly.
Frequently Asked Questions
How does SectionKit know which cell class to instantiate?
SectionKit relies on the register(_:) method called inside config(sectionView:) to map cell classes to reuse identifiers. When you call dequeue(at:) in item(at:), the helper uses the identifier associated with the class you specify, ensuring the collection view returns the correct cell type.
Can I use more than three cell types in one section?
Yes. The enum-based pattern scales to any number of cell types. Simply add new cases to your CellType enum, register the additional classes in config(sectionView:), and add corresponding branches to your switch statements in itemSize(at:) and item(at:).
Do I need to handle cell reuse identifiers manually?
No. SKCSectionActionProtocol abstracts reuse identifiers away. The register(_:) and dequeue(at:) methods manage the mapping between Swift types and UICollectionView reuse identifiers automatically, reducing boilerplate and preventing identifier collisions.
Where is the best place to handle cell selection logic?
Implement the item(selected:) method from SKCSectionProtocol. Inside this method, switch on the CellType enum at the provided index to execute type-specific behavior, such as navigation or state updates, without cluttering your view controller.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →