Level up on protocols, associatedtypes and extensions.

In the previous blog post, we learnt about basic use of associatedtype. In this blog post, we will see some advance use of protocol, associatedtype and extension by building a generic UITableview datasource.

Lets get started

How many times we get a requirement to show a simple list of data in a UITableview in single section. Sometimes we need to do this multiple times in one application. Just because we have different types of data sets, we have to implement UITableView datasource again and agian but the implementation usually has same logic.

Consider we need to show a list of Person in a table view. Below is the pseudo-implementation.

//Datasource setup to to show Person
var persons: [Person]

func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return persons.count
    }
    
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath) 
        cell.confgiure(forPerson person: persons[indexPath.row])
        return cell
    }

Now at some other place, wee need to show a list of department. The logic to feed our tableView is same but still, we need to write that logic again with different data type i.e. Department.

//Datasource setup to show Department
var departments: [Department]

func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return departments.count
    }
    
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath) 
        cell.confgiure(forDepartment department: departments[indexPath.row])
        return cell
    }

So the idea behind this generic UITableView datasource is to put this same logic at one place which can be reused for different types of data.

Lets start with creating a simple protocol which denotes that the conforming data type can be used to populate a UITableViewCell.

Pardon my naming conventions. I am not good in naming anything 😄.

protocol TableViewCellDataRepresentable {}

Now lets create a protocol, which denotes that the conforming types can decorate a specific UITableViewCell with a specific TableViewCellDataRepresentable.

Note: since the decorator does not know the actual type of UITableViewCell and TableViewCellDataRepresentable, so we need to use a placeholder type. associatedtype can serve this purpose well.

protocol TableViewCellDecorator {
    associatedtype DataType: TableViewCellDataRepresentable
    associatedtype CellType: UITableViewCell
    static func update(tableViewCell cell: CellType, withData data: DataType)
}

So the conforming type will define its DataType and CellType and implement the logic to decorate the cell accordingly.

Now lets begin implementation of our generic UITableView datasource.

Try to stay with me, its a little bit complex to keep up.

class TableViewHandler<T: TableViewCellDecorator>: NSObject, UITableViewDataSource {
    
    fileprivate(set) var dataSet: [T.DataType]
    private(set) weak var tableView: UITableView!
    let reuseIdentifiers: (IndexPath) -> String
    
    init(withTableView tableView: UITableView, dataSet: [T.DataType], reuseIdentifiers: @escaping (IndexPath) -> String) {
        self.tableView = tableView
        self.dataSet = dataSet
        self.reuseIdentifiers = reuseIdentifiers
        super.init()
        self.tableView.dataSource = self
    }
    
    func reloadWithData(data: [T.DataType]) {
        dataSet = data
        tableView.reloadData()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSet.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifiers(indexPath), for: indexPath) as? T.CellType else {
           return tableView.dequeueReusableCell(withIdentifier: reuseIdentifiers(indexPath), for: indexPath)
        }
        T.update(tableViewCell: cell, withData: dataSet[indexPath.row])
        return cell
    }
}

We created a generic TableViewHandler whose genric type T should conform to TableViewCellDecorator.

Next we decalared a dataSet whose type is T's DataType. That is, dataSet will be collection of TableViewCellDataRepresentable whose actual type will be known at the time of declaring and intialising our TableViewHandler object.

This kind of coupling ensures that we wont be able to pass wrong data type and also there is no type casting involved. Everything will be known at compile time itslef.

It also has a closure property names reuseIdentifiers, in which the client can pass cellIdentifiers based on indexpath.

Finally, in tableView’s datasoure method:

  • We are returning dataSet count as numberOfRows
  • Next in cellForRowAtIndexPath, we are getting the cell identifier by invoking reuseIdentifiers closure and also making sure that tableView returns a cell which our T i.e. TableViewCellDecorator can decorate.

Example

Its time to see it in action with an example.

First we will do basic setup by creating a cell and UIViewController with tableView.

The is the layout of my cell, which has three labels horizontally aligned inside a stack view.

Image alt

I have named it…….ThreeLabelTableViewCell 😛. Again, pardon my naming.

There is nothing in the implementation of ThreeLabelTableViewCell.

This is how my Storyboard looks like. Nothing extra ordinary.

Image alt

Now lets create our modal.

struct Person {
    let firstName: String
    let lastName: String
    var age: Int
    var isVIP: Bool
}

extension Person: TableViewCellDataRepresentable {}

extension Person { 
    static let vishal = Person(firstName: "Vishal Singh", lastName: "Panwar", age: 28, isVIP: false /*yeah, no VIP flag for me, I like to keep myself low ✌ */)
    static let bruce = Person(firstName: "Bruce", lastName: "Wayne", age: 35, isVIP: true)
    static let john = Person(firstName: "John", lastName: "Cena", age: 30, isVIP: true)
    static let tony = Person(firstName: "Tony", lastName: "Stark", age: 35, isVIP: true)
    static let peter = Person(firstName: "Peter", lastName: "Parker", age: 20, isVIP: true)
    static let clark = Person(firstName: "Clark", lastName: "Kent", age: 120, isVIP: false /*I don't like superman, no VIP treatement for him*/))
}

We created a struct called Person. Next we created an extension of Person which conforms to TableViewCellDataRepresentable. Another extension just has some helper properties which we will be going use shortly.

That was all for our basic setup. The only new thing we need to do before we go into implementation of our ViewController is a TableViewCellDecorator which can decorate ThreeLabelTableViewCell using Person.

struct HorizontalDecorator: TableViewCellDecorator {
    static func update(tableViewCell cell: ThreeLabelTableViewCell, withData data: Person) {
        cell.stackView.layoutMargins = UIEdgeInsets(top: 0, left: cell.stackView.spacing, bottom: 0, right: cell.stackView.spacing)
        cell.stackView.isLayoutMarginsRelativeArrangement = true
        cell.toggle(VIPTheme: data.isVIP)
        cell.stackView.axis = .horizontal
        cell.firstLabel.text = data.firstName
        cell.secondLabel.text = data.lastName
        cell.thirdLabel.text = String(data.age)
    }
}

In above TableViewCellDecorator, the CellType and DataType are inferred from the paramter types of update function. So the complier has the concrete type for CellType as ThreeLabelTableViewCell and DataType as Person. Also there is no type casting involved as we won’t be able to pass any other cell than ThreeLabelTableViewCell and any other dataType then Person.

Go through it once again, its not that complex.

If you are wondering about toggle(VIPTheme:) function, below is the snippet of all the helpers functions which I am using for this blog post.

extension ThreeLabelTableViewCell {
    
    func toggle(VIPTheme isVIP: Bool) {
        firstLabel.textColor = isVIP ? UIColor.VIP : UIColor.commonMan
        secondLabel.textColor = isVIP ? UIColor.VIP : UIColor.commonMan
        thirdLabel.textColor = isVIP ? UIColor.VIP : UIColor.commonMan
        firstLabel.font = isVIP ? UIFont.VIP : UIFont.commonMan
        secondLabel.font = isVIP ? UIFont.VIP : UIFont.commonMan
        thirdLabel.font = isVIP ? UIFont.VIP : UIFont.commonMan
    }
}

extension UIColor {
    static let VIP = UIColor.init(colorLiteralRed: 255/255, green: 215/255, blue: 0, alpha: 1)
    static let commonMan = UIColor.black
}

extension UIFont {
    static let VIP = UIFont.boldSystemFont(ofSize: 17)
    static let commonMan = UIFont.systemFont(ofSize: 17)
}

We will now see, that our TableHandler does not need to have any information about Person and ThreeLabelTableViewCell to display data from Person on ThreeLabelTableViewCell. Thats crazy enough for me to be fan of protocol and associatedtype in Swift 👍.

This also means that its very easy to hook up any other combination of decorator, cell and datatype with our TableViewHandler.

Now lets implement our ViewController.

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    
    private var persons = [Person.vishal, Person.john]

    private lazy var tableViewDatasource: TableViewHandler<HorizontalDecorator> = {
        let tableHander = TableViewHandler<HorizontalDecorator>(withTableView: self.tableView, dataSet: self.persons, reuseIdentifiers: { _ in return "CellID" })
        return tableHander
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Persons"
        setUpTableView()
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    private func setUpTableView() {
        tableView.register(UINib.init(nibName: "ThreeLabelTableViewCell", bundle: nil), forCellReuseIdentifier: "CellID")
        tableView.estimatedRowHeight = 44
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.dataSource = tableViewDatasource
    }
}

Thats it. Thats all in our ViewController. This is how it looks when we run it.

Image alt

Yea, not one of the most gorgeous user interface. Naming things is not the only thing in which I am not good at, but hey, its just a demo, so don’t judge.

We can easily create a new decorator to change the look of our TableView.

struct VerticalDecorator: TableViewCellDecorator {
    static func update(tableViewCell cell: ThreeLabelTableViewCell, withData data: Person) {
        cell.stackView.layoutMargins = UIEdgeInsets(top: cell.stackView.spacing, left: 0, bottom: cell.stackView.spacing, right: 0)
        cell.stackView.isLayoutMarginsRelativeArrangement = true
        cell.toggle(VIPTheme: data.isVIP)
        cell.stackView.axis = .vertical
        cell.firstLabel.text = data.firstName
        cell.secondLabel.text = data.lastName
        cell.thirdLabel.text = String(data.age)
    }
}

This is a VerticalDecorator, which will layout views of our ThreeLabelTableViewCell differently using same Person modal.

We just need to change the declaration of our TableViewHandler as below.

private lazy var tableViewDatasource: TableViewHandler<VerticalDecorator> = {
        let tableHander = TableViewHandler<VerticalDecorator>(withTableView: self.tableView, dataSet: self.persons, reuseIdentifiers: { _ in return "CellID" })
        return tableHander
    }()

Thats how our table looks now.

Image alt

I know its looking even worse now.

But the idea is, you can use any combination of data type and cell with TableViewHandler. You can use another TableViewCellDecorator which decorates some other cell with Person data or which decorates ThreeLabelTableViewCell with some other data type or some other cell with some other data type. I hope you get my point 🙄.

Extending TableViewHandler.

Apart from simply displaying data on table view, another most common operation we do is adding rows with new data. This is again something which has common logic but we write it everytime we have to do it.

//In TableViewHandler class, add this method.
func addData(data: [T.DataType], animated: Bool = true) {
        let startIndex = dataSet.count
        var indicesToAdd: [IndexPath] = []
        dataSet.append(contentsOf: data)
        if animated {
            for index in startIndex..<(data.count + startIndex) {
                indicesToAdd.append(IndexPath(row: index, section: 0))
            }
            tableView.beginUpdates()
            tableView.insertRows(at: indicesToAdd, with: .fade)
            tableView.endUpdates()
        } else {
            tableView.reloadData()
        }
    }

Its again data type independent. It will append new data and accordingly add new cells at the bottom of tableView.

While adding new cells was straight forward, deleting existing data and cells is little bit ticky. The implementation of removing data should somewhat look like below.

//In TableViewHandler class, add this method.

func removeData(data: [T.DataType], animated: Bool = true) {
        var indicesToRemove: [IndexPath] = []
        let proxyDataSet = dataSet
        for index in 0..<data.count {
            guard let _indexToRemove = proxyDataSet.index(where: { return $0 == data[index] }),
                let currentIndex = dataSet.index(where: { return $0 == data[index] })else {
                    continue
            }
            dataSet.remove(at: currentIndex)
            indicesToRemove.append(IndexPath(row: _indexToRemove, section: 0))
        }
        if animated {
            tableView.beginUpdates()
            tableView.deleteRows(at: indicesToRemove, with: .fade)
            tableView.endUpdates()
        } else {
            tableView.reloadData()
        }
    }


Could you identify the problem?

The issue is, == operator. Compiler does not know how to perform equality check on TableViewCellDataRepresentable rightly because we haven’t provided any information regarding this to compiler. Hence above code won’t compile.

What can we do?

Extension for rescue.

I will just go ahead and write the required code.

extension TableViewHandler where T.DataType: Equatable {
    
    func removeData(data: [T.DataType], animated: Bool = true) {
        var indicesToRemove: [IndexPath] = []
        for index in 0..<data.count {
            let indexToRemove = dataSet.index() { return $0 == data[index] }
            guard let _indexToRemove = indexToRemove else {
                continue
            }
            dataSet.remove(at: _indexToRemove)
            indicesToRemove.append(IndexPath(row: _indexToRemove, section: 0))
        }
        if animated {
            tableView.beginUpdates()
            tableView.deleteRows(at: indicesToRemove, with: .fade)
            tableView.endUpdates()
        } else {
            tableView.reloadData()
        }
    }
}

Now the removeData function is only avalibale if the T.DataType, i.e TableViewCellDataRepresentable conforms to Equatable.

In our case, Person does not conform to Equatable so we wont be even able to call removeData. Thats mind blowing.

If we want removeData to be available for us, we can simply confrom our concrete TableViewCellDataRepresentable to Equatable like below.

extension Person: Equatable {
    static func == (lhs: Person, rhs: Person) -> Bool {
        return (lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName)
    }
}

Now we can call removeData as well.

One last thing.

While adding new data, what if we want to perform duplicate check and just update the old version with new version if it already exist otherwise simply append it at bottom.

protocol Updatable: Equatable {
    mutating func update(fromCopy copy: Self)
}

// Still `Equatable` as `Updatable` inherits `Equatable`.
extension Person: Updatable {
    mutating func update(fromCopy copy: Person) {
        age = copy.age
        isVIP = copy.isVIP
    }
    static func == (lhs: Person, rhs: Person) -> Bool {
        return (lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName)
    }
}

extension TableViewHandler where T.DataType: Updatable {
   
    func addData(data: [T.DataType], updateExisting: Bool, animated: Bool = true) {
        var indicesToAdd: [IndexPath] = []
        var indicesToUpdate: [IndexPath] = []
        for index in 0..<data.count {
            let startIndex = dataSet.count
            let indexToUpdate = dataSet.index() { return $0 == data[index] }
            if let _indexToUpdate = indexToUpdate {
                dataSet[_indexToUpdate].update(fromCopy: data[index])
                indicesToUpdate.append(IndexPath(row: _indexToUpdate, section: 0))
                continue
            } else {
                indicesToAdd.append(IndexPath(row: startIndex, section: 0))
                dataSet.append(data[index])
            }
        }
        if animated {
            tableView.beginUpdates()
            tableView.insertRows(at: indicesToAdd, with: .fade)
            tableView.reloadRows(at: indicesToUpdate, with: .fade)
            tableView.endUpdates()
        } else {
            tableView.reloadData()
        }
    }
}

This is the basic setup I use for basic TableView operations.

Thats it for this blog post. Let me know if you have any queries.

Source Code

Happy Swifting.🃏

Written on December 11, 2016