UITableView Datasource with SwiftUI-style syntax

Gwenn Guihal
Oct 2 · 6 min read
Photo by Messala Ciulla on Unsplash

Introduction

SwiftUI seems to be great but unfortunately developers will be able to use it only if they target their app on ios13.
But SwiftUI is built on top of some new swift 5.1 features that can be used with previous iOS versions.
It’s the case of @functionBuilder(Swift Evolution Proposal).

Some people have already explained the power of @functionBuilder, I invite you to read them if you need more information:

The more I like with SwiftUI is the readability of the UI implementation. And It’s possible thanks to @functionBuilder.
I have been working for a few years on a library called Collor which provides a data-driven datasource for UICollectionView and several other features (diffing, decoration view handling, …):

A datasource of a collectionView build with Collor looks like this:

When I saw first examples made with SwiftUI, I thought, why not do it on Collor ? It’s pretty much the same idea: an UI declarative implementation (or a kind of DSL)!
Because it’s a lot of work, I preferred to start the experimentation with UITableView, much simpler.

At the end of this article, a datasource of an UITableView will look like this:

For this result:

Comparing to Collor’s datasource, all notions of appends have been removed for better readability. We are close to the swiftUI syntax:

List(landmarks) { landmark in
HStack {
Image(landmark.thumbnail)
Text(landmark.name)
Spacer()

if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}

How does it work ?

There is a kind of magic behind @functionBuilder. What I understand is that each line into the closure is used as an argument of the function, here the initialiser. For example:

Section {            
Spacer()
LabelCell( TitleAdapter(label: item.title) )
}

To really really really summarise, the compiler transforms these lines into :

Section(contents: [Spacer(), LabelCell(TitleAdapter(label: item.title))])

To do that, it is using this @functionBuilder:

@_functionBuilder
public struct ContentBuilder {
public static func buildBlock(_ contents: Content?...) -> Content {
return SomeThing
}
}

Everything is Content

First let’s create a new protocol Content. Content may contain an array of Content:

public protocol Content {
var contents: [Content]? { get }
}
public extension Content {
var contents: [Content]? {
nil
}
}

Then create a concrete type of Content, a Container:

public struct Container: Content {
public var contents: [Content]?
public init(contents: [Content] = []) {
self.contents = contents
}
}

Once we have that, we can finally create our own @functionBuilder. According the parameter, we create a Container which contains one or several contents.

@_functionBuilder
public struct ContentBuilder {

// only one Content
public static func buildBlock(_ content: Content) -> Content {
return Container(contents: [content] )
}

// an array of Content
public static func buildBlock(_ contents: Content?...) -> Content {
return Container(contents: contents.compactMap { $0 } )
}
}

And use it in the Container struct:

extension Container {
public init(@ContentBuilder _ builder: () -> Content) {
self.init(contents: builder().contents ?? [])
}
}

Now, we are able to create some Container by using the @functionBuilder syntax:

let container = Container {
Container {
Container()
}
Container {
Container {
Container()
Container()
}
}
}

Interesting, isn’t ?

UITableView Datasource

A tableView datasource is composed by sections which contain themselves cells.
Of course, Section and Cell implement the Content protocol:

public struct Section: Content {
var cells: [Int : Cell] // indexPath.row, Cell
}
public protocol Cell: class, Content {
var identifier: String { get }
func getAdapter() -> Adapter
}

Cell is a protocol because we prefer protocol-oriented programming than inheritance.
A Cell is a description of an UITableViewCell. As we are using Storyboard, only the identifier property is required to create or reuse a cell.

An adapter is a viewModel, its job is to convert some inputs in a human readable output, for example a date:

protocol LabelAdapter: Adapter {
var label: NSAttributedString { get }
}
struct DateAdapter: LabelAdapter {
let label: NSAttributedString
let dateFormatter = DateFormatter(..)
init(date: Date) {
label = dateFormatter.string(from: date).style(//...)
}
}

Finally, the DataSource class is just a container of sections that implements UITableViewDataSource protocol:

We just have to create a convenience initalizer to use it with @functionBuilder:

public extension DataSource {
convenience init(@ContentBuilder _ builder: () -> Content) {
self.init()
var sections = [Section]()
Self.invalidate(contents: [builder()], sections: &sections)
self.sections = sections
}
}

Because a content could contain some contents (Container), we use a recursive method to build sections of the datasource:

private static func invalidate(contents: [Content], sections: inout [Section]) {
contents.forEach { content in
switch (content, content.contents) {
case (let section as Section, _):
sections.append(section)
case (_, .some(let contents)):
invalidate(contents: contents, sections: &sections)
default:
break
}
}
}

Loop, Map, CompactMap

I said before everything is Content. It’s even more true when we need a loop in our code. For example @functionBuilder doesn’t allow that:

model.items.forEach { item in
LabelCell(TitleAdapter(label: item.title))
}

The compiler doesn’t understand this code because it just want you return Content and nothing else. So, we have to implement our own ForEach, Map or CompactMap.
These structs also implements the Content protocol.
The following code is pretty understandable. You give a collection and a closure that transforms the collection elements into Content(s) and that’s it!

Which gives us:

Map(model.items) { item -> Content in
LabelCell(TitleAdapter(label: item.title))
}

If let …

if let is not allowed either. Like the object Map, we need to create an IfLet object that return or not a Content according the input.
As always, IfLet implements the protocol Content.

To use it as such:

IfLet(item.subTitle) { (subTitle) -> Content in
LabelCell(SubtitleAdapter(label: subTitle))
}

More functional syntax

I was inspired by RxSwift to have a more functional and more swifty implementation:

Map:

model.items.ui.map { item in
// ...
}

IfLet (using the same idea of the Optional.map()):

item.subTitle.ui.map {
// ...
}

I don’t explain the code here, you can have a look a the github project if you are curious.

Conclusion

I hope you enjoyed this article, it’s a bunch of ideas but it’s not ready to go in production. @functionBuilder is a little magical and not well documented yet. I’ve been groping a lot to have something that works. Furthermore, the compiler doesn’t help you, error message are often mistaken.

Sources are available on github:

My next job is to add @functionBuilder feature on Collor in order to create a collectionViewDataSource like this:

let dataSource = DataSource {Section {
LabelCell(...)
ImageCell(...)
Spacer()
Decoration(.purple) {
Space()
DescriptionCell(...)
Decoration(.form) {
Space()
TextFieldCell(...)
}
}
}
}

Thanks for reading, and let me know what you think on Twitter.

Gwenn Guihal

Written by

Lead iOS developer at oui.sncf, Paris / Indie game developer (cocos2d-X). Creator of YesSir and Troisix.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade