By using SectionKit each section in a UICollectionView
is implemented separately, so you can keep your classes small and maintainable.
Sections can be combined like building blocks and creating screens with otherwise complex datasources becomes manageable.
At Trade Republic we are using SectionKit extensively. It powers most of our screens, with some of them containing up to 30 different types of sections. By combining SectionKit with ReactiveSwift and a reactive network protocol we are able to provide truly dynamic experiences.
This library is inspired by IGListKit, but it is implemented in Swift and it offers a type safe API through the use of generics.
To see SectionKit in action please check out the example project.
Contents:
- Click File > Swift Packages > Add Package Dependency...
- Use the package URL
https://github.com/traderepublic/SectionKit
to add SectionKit/DiffingSectionKit to your project.
On the package:
dependencies: [
.package(name: "SectionKit", url: "https://github.com/traderepublic/SectionKit", from: "1.0.0")
]
On a target:
dependencies: [
.product(name: "SectionKit", package: "SectionKit"),
.product(name: "DiffingSectionKit", package: "SectionKit") // optionally, includes diffing via DifferenceKit
]
Add this to your Cartfile
:
github "traderepublic/SectionKit" ~> 1.0
Note: Since the xcframework variant of
DifferenceKit
is linked against, make sure to build Carthage dependencies using the--use-xcframeworks
option. For more information please visit the Carthage repository.
To get started, we need to initialise a CollectionViewAdapter
.
That object handles the communication to and from the UICollectionView
.
Since we want to have multiple sections we'll use the ListCollectionViewAdapter
,
but if there would only be a single section we could also use the SingleSectionCollectionViewAdapter
.
Code
import SectionKit
final class MyCollectionViewController: UIViewController {
private var collectionViewAdapter: CollectionViewAdapter!
private let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout()
)
override func loadView() {
view = collectionView
}
override func viewDidLoad() {
super.viewDidLoad()
collectionViewAdapter = ListCollectionViewAdapter(
viewController: self,
collectionView: collectionView,
dataSource: self // no worries, we're going to add conformance to the protocol in a bit
)
}
}
In this example we're going to have two sections and we'll now define their respective models FirstSectionModel
and SecondSectionModel
:
Code
struct FirstSectionModel {
let items = ["Hello", "world"]
}
struct SecondSectionModel {
let item = "Single item"
}
The next thing we want to do is to implement their corresponding SectionController
.
In this example, the first section shows a list of strings and the second section shows a single string.
For both cases there are base classes we can inherit from:
Code
class FirstSectionController: ListSectionController<FirstSectionModel, String> {
override func items(for model: FirstSectionModel) -> [String] {
model.items
}
override func cellForItem(at indexPath: SectionIndexPath, in context: CollectionViewContext) -> UICollectionViewCell {
// cell types are automatically registered when dequeuing
let cell = context.dequeueReusableCell(StringCell.self, for: indexPath)
cell.label.text = items[indexPath]
return cell
}
override func sizeForItem(at indexPath: SectionIndexPath, using layout: UICollectionViewLayout, in context: CollectionViewContext) -> CGSize {
CGSize(width: context.containerSize.width, height: 50)
}
}
class SecondSectionController: SingleItemSectionController<SecondSectionModel, String> {
override func item(for model: SecondSectionModel) -> String? {
model.item
}
override func cellForItem(at indexPath: SectionIndexPath, in context: CollectionViewContext) -> UICollectionViewCell {
// cell types are automatically registered when dequeuing
let cell = context.dequeueReusableCell(StringCell.self, for: indexPath)
cell.label.text = item
return cell
}
override func sizeForItem(at indexPath: SectionIndexPath, using layout: UICollectionViewLayout, in context: CollectionViewContext) -> CGSize {
CGSize(width: context.containerSize.width, height: 50)
}
}
At last we want to implement the ListCollectionViewAdapterDataSource
protocol. Here we're providing the SectionController
for each model.
The sections both need a unique identifier, so the underlying SectionController
can be recycled, which is necessary for animating updates.
Please note: Although the identifier needs to be unique across the list of sections, it also needs to be persistent across updates so the previous
SectionController
can be reused.
Code
enum MyCollectionSectionId: Hashable {
case first
case second
}
protocol MyCollectionSection {
var sectionId: MyCollectionSectionId { get }
}
extension FirstSectionModel: MyCollectionSection {
var sectionId: MyCollectionSectionId { .first }
}
extension SecondSectionModel: MyCollectionSection {
var sectionId: MyCollectionSectionId { .second }
}
extension MyCollectionViewController: ListCollectionViewAdapterDataSource {
// this can be implemented in a viewmodel instead
private func createSectionModels() -> [MyCollectionSection] {
[FirstSectionModel(), SecondSectionModel()]
}
func sections(for adapter: CollectionViewAdapter) -> [Section] {
createSectionModels().compactMap {
switch $0 {
case let model as FirstSectionModel:
return Section(
id: model.sectionId,
model: model,
controller: FirstSectionController(model: model)
)
case let model as SecondSectionModel:
return Section(
id: model.sectionId,
model: model,
controller: SecondSectionController(model: model)
)
default:
assertionFailure("\(#function): unknown section model: \($0)")
return nil
}
}
}
}
That's it! Since both sections are completely decoupled from each other, they can be easily reused in other places in the app and writing unit tests becomes much easier!
As a final bonus, if you want animated updates, you can use the DiffingListSectionController
(import DiffingSectionKit) instead of the "normal" ListSectionController
.
If you're running iOS 13+ you may also use the FoundationDiffingListSectionController
that is already contained in the SectionKit module.