Issue #598

It’s hard to see any iOS app which don’t use UITableView or UICollectionView, as they are the basic and important foundation to represent data. UICollectionView is very basic to use, yet a bit tedious for common use cases, but if we abstract over it, then it becomes super hard to customize. Every app is unique, and any attempt to wrap around UICollectionView will fail horribly. A sensable approach for a good abstraction is to make it super easy for normal cases, and easy to customize for advanced scenarios.

I’m always interested in how to make UICollectionView easier and fun to write and have curated many open sources here data source. Many of these data source libraries try to come up with totally different namings and complex paradigm which makes it hard to onboard, and many are hard to customize.

In its simplest form, what we want in a UICollectionView data source is cell = f(state) , which means our cell representation is just a function of the state. We just want to set model to the cell, the correct cell, in a type safe manner.

Generic data source

The basic is to make a generic data source that sticks with a particular cell

1

2

3

4

5

class DataSource < T >: NSObject {

let items: [ T ]

let configure: ( T , UICollectionViewCell ) -> Void

let select: ( UICollectionViewCell , IndexPath ) -> Void

}



This works for basic usage, and we can create multiple DataSource for each kind of model. The problem is it’s hard to subclass DataSource as generic in Swift and inheritance for ObjcC NSObject don’t work well.

Check for the types

Seeing the problem with generic data source, I’ve tried another approach with Upstream where it’s easier to declare sections and models.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

let sections: [ Section ] = [

Section (

header: Header (model: Model .header( "Information" ), viewType: HeaderView . self ),

items: [

Item (model: Model .avatar(avatarUrl), cellType: AvatarCell . self ),

Item (model: Model .name( "Thor" ), cellType: NameCell . self ),

Item (model: Model .location( "Asgard" ), cellType: NameCell . self )

]

),

Section (

header: Header (model: Model .header( "Skills" ), viewType: HeaderView . self ),

items: [

Item (model: Model .skill( "iOS" ), cellType: SkillCell . self ),

Item (model: Model .skill( "Android" ), cellType: SkillCell . self )

]

)

]



adapter.reload(sections: sections)



This uses the Adapter pattern and we need to handle AdapterDelegate . To avoid the generic problem, this Adapter store items as Any , so we need to type cast all the time.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

extension ProfileViewController : AdapterDelegate {

func configure (model: Any , view: UIView, indexPath: IndexPath) {

guard let model = model as ? Model else {

return

}



switch (model, view) {

case (.avatar( let string), let cell as Avatarcell ):

cell.configure(string: string)

case (.name( let name), let cell as NameCell ):

cell.configure(string: name)

case (.header( let string), let view as HeaderView ):

view.configure(string: string)

default :

break

}

}

}



The benefit is that we can easily subclass this Adapter manager to customize the behaviour, here is how to make accordion

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class AccordionManager < T >: Manager < T > {

private var collapsedSections = Set < Int >()



override func tableView ( _ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

return collapsedSections. contains (section)

? 0 : sections[section].items. count

}



func toggle (section: Int) {

if collapsedSections. contains (section) {

collapsedSections.remove(section)

} else {

collapsedSections.insert(section)

}



let indexSet = IndexSet (integer: section)

tableView?.reloadSections(indexSet, with: .automatic)

}

}



SwiftUI

SwiftUI comes in iOS 13 with a very concise and easy to use syntax. SwiftUI has good diffing so we just need to update our models so the whole content will be diffed and rendered again.

1

2

3

4

5

6

7

8

9

10

11

12

var body: some View {

List {

ForEach (blogs) { blog in

VStack {

Text (blog.name)

}

.onTap {

print ( "cell was tapped" )

}

}

}

}



SwiftUI style with diffing

I built DeepDiff before and it was used by many people. Now I’m pleased to introduce Micro which is a SwiftU style with DeepDiff powered so it performs fast diffing whenever state changes.

With Micro we can just use the familiar forEach to declare Cell , and the returned State will tell DataSource to update the UICollectionView .

Every time state is assigned, UICollectionView will be fast diffed and reloaded. The only requirement is that your model should conform to DiffAware with diffId so DeepDiff knows how to diff for changes.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

let dataSource = DataSource (collectionView: collectionView)

dataSource.state = State {

ForEach (blogs) { blog in

Cell < BlogCell >() { context, cell in

cell.nameLabel.text = blog.name

}

.onSelect { context in

print ( "cell at index \(context.indexPath.item) is selected" )

}

.onSize { context in

CGSize (

width: context.collectionView.frame.size.width,

height: 40

)

}

}

}



DataSource is completely overridable, if you want to customize any methods, just subclass DataSource , override methods and access its state.models

1

2

3

4

5

6

class CustomDataSource : DataSource {

override func collectionView ( _ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

let blog = state.models[indexPath.item] as ? Blog

print (blog)

}

}



Diffable data source in iOS 13

In iOS 13, Apple adds Using Collection View Compositional Layouts and Diffable Data Sources which is very handy.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

func makeDataSource () -> UITableViewDiffableDataSource < Section , Contact > {

let reuseIdentifier = cellReuseIdentifier



return UICollectionViewDiffableDataSource (

collectionView: collectionView,

cellProvider: { collectionView, indexPath, blog in

let cell = tableView.dequeueReusableCell(

withIdentifier: reuseIdentifier,

for : indexPath

)



cell.textLabel?.text = blog.name

cell.detailTextLabel?.text = blog.email

return cell

}

)

}



This is iOS 13+ only, and the main components are the cellProvider acting as cellForItemAtIndexPath , and the snapshot for diffing. It also supports section.