Writing better UITableView code with Swift Generics
I was obsessed with Protocol Oriented Programming, read a lot of blogs, watched so many videos and got some idea behind its implemetation and working. So now I know the basic protocol based programming stuff in Swift (the new cool 😎), but I was still struggling to use POP in our current codebase. Tried, tired and again tried but couldn’t get through.
I tried the other way of writing reusable code and converted some of our codebase classes to follow this pattern.
Swift + Generics + UITableViewController
In most of the UITableViewController(TVC) subclasses or in UIViewController containing UITableView we override/implement these three methods of UITableViewDataSource
Every TVC we create from now will be a subclass of GenericTVC, so that we don’t have to implement these two methods in each one of them. Next step is returning values from numberOfSections and numberOfRows and for this we will make a generic public dictionary in GenericTVC, you may want to add a 2D Swift array [[String]]. Add whatever is convenient for you.
vardataSource:[String:[Item]]?// Item here is a model class which we use in our codebase. You can use anything of your choice.
GenericTVC with implementations of these methods :
classGenericTVC:UITableViewController{vardatasource:[String:[Item]]?overridefuncviewDidLoad(){super.viewDidLoad()}// MARK: - Table view data sourceoverridefuncnumberOfSectionsInTableView(tableView:UITableView)->Int{ifletdatasource=datasource{returndatasource.keys.count}else{return0}}overridefunctableView(tableView:UITableView,numberOfRowsInSectionsection:Int)->Int{ifletdatasource=datasource{/*
Casting the section to String so as to get a key for datasource values
Int can also be used but its all your choice.
Which ever section you want in the tableView first, add it as :
self.datasource["0"] = [Item]()
and so on..
*/letsectionString=String(section)ifletitems=datasource[sectionString]{returnitems.count}}return0}}
With this we have number of sections and also the number of rows in every section of our tableViews.
The only method left is cellForRowAtIndexPath: which we are not going to implement in this GenericTVC(again, you can implement this too if you have same configuration of table view cells in your app) instead we will override this method in each of our GenericTVC subclass.
We have our FirstTableViewController and a custom tableView cell FirstTableViewCell
This code compiles and runs as expected, but we are not expected to write this in production. Lets refactor this code.
The call to tableView.registerClass will be repeated for every class lets push it to superclass, GenericTVC. But what if we have a tableView which has multiple kinds of cell. Mmmmmm.. lets create another public property in GenericTVC
self.reuseClasses=[FirstTableViewCell.self]// or may be self.reuseClasses = [FirstTableViewCell.self, SecondTableViewCell.cell, ThirdTableViewCell.self]super.viewDidLoad()
The above code is self explanatory.
Now come back to cellForRowAtIndexPath: in FirstTableViewController. Looks bad!. Lets move the cell initialization code to our base class. Add this method to GenericTVC:
This piece of code works fine but I still don’t like the downcast from UITableViewCell to FirstTableViewCell. Lets solve this by Generics.
Generics to the rescue
With generics in Swift we can define methods(not only methods, actually everything 😉) which can accept generic parameters and return generic types. So lets use this feature for our good.
In GenericTVC replace the implementation of reusableCellFor.... with the below code.
Wow, this looks fantastic 🤑🤑 , but what is it 🤔🤔?
<CustomTVC:UITableViewCell>/*
This part of the method tells the compiler that here I introduce you to a generic type CustomTVC which is a subclass of UITableViewCell
so do not treat future occurrences of this as errors.
*/reuseClass:CustomTVC.Type/*
Accept a type which is a CustomTVC(UITableViewCell subclass)
*/
Everything else is self explanatory I guess 🤓.
Finally in FirstTableViewController remove the as FirstTableViewCell and the also remove the if let check. And to keep the subclasses cleaner lets create a helper method in GenericTVC for getting the Item object from the datasource for current indexPath.
classGenericTVC:UITableViewController{vardatasource:[String:[Item]]?varreuseClasses:[AnyClass]?overridefuncviewDidLoad(){super.viewDidLoad()ifletclasses=reuseClasses{forreuseClassinclasses{self.tableView.registerClass(reuseClass,forCellReuseIdentifier:String(reuseClass))}}}// MARK: - Table view data sourceoverridefuncnumberOfSectionsInTableView(tableView:UITableView)->Int{ifletdatasource=datasource{returndatasource.keys.count}else{return0}}overridefunctableView(tableView:UITableView,numberOfRowsInSectionsection:Int)->Int{ifletdatasource=datasource{/*
Casting the section to String so as to get a key for datasource values
Int can also be used but its all your choice.
Which ever section you want in the tableView first, add it as :
self.datasource["0"] = [Item]()
and so on..
*/letsectionString=String(section)ifletarray=datasource[sectionString]{returnarray.count}}return0}funcreusableCellFor<CustomTVC:UITableViewCell>(tableViewtableView:UITableView,reuseClass:CustomTVC.Type)->CustomTVC{ifletcell=tableView.dequeueReusableCellWithIdentifier(String(reuseClass))as?CustomTVC{returncell}else{returnCustomTVC(style:.Subtitle,reuseIdentifier:String(reuseClass))}}funcitemFor(indexPathindexPath:NSIndexPath)->Item?{ifletdatasource=self.datasource{letsectionString=String(indexPath.section)ifletarray=datasource[sectionString]{returnarray[indexPath.row]}}returnnil}}