It’s no secret that working with the UITableView is verbose and inconvenient. It’s been around since iOS 2.0 and it feels just as archaic. Then, Apple graced us with the UICollectionView in iOS 6.0 and it was indeed much awaited, but the API’s were inconsistent with the UITableView and still felt just as irritating. In this post, I’d like to reconcile the two API’s and add some sugar to make it more pleasant to work with these commonly used controls.
The Table and Collection Family Tree
The UITableView and UICollectionView share an ancestor: UIScrollView. This is the best relationship Apple decided to give between the two; yippie it scrolls! Instead, I’d like to take on another perspective; they both are containers of data. So the first thing I’d like to do is create a new protocol called “DataViewable” and force the table and collection views to conform to it (see my previous post about Protocol Conformance Extensions):
public protocol DataViewable { func reloadData() } extension UITableView: DataViewable {} extension UICollectionView: DataViewable {}
Both the table and collection views already have the “reloadData” function, but strangely don’t exist in any of their shared ancestors. Now that I’ve forced them to conform to my new “DataViewable” protocol, I can call this essential function on either view without knowing whether it is a table or collection.
Extending Table and Collection Controllers
When creating an app that has table and collection screens for the same data, it is difficult to create extensions with shared logic. The reason is because the table and collection API’s are so close, but are inconsistent nor do they share data-aware ancestors or protocols. Now with the new “DataViewable” protocol in place, I could execute logic against tables and collections interchangeably.
For example, say I’m feeding the same data to a UITableViewController screen and a UICollectionViewController screen. I can have them conform to a protocol and extend it with data population and reloading. See the new “DataControllable” protocol below:
public protocol DataControllable: class { var models: [Contentable] { get set } var dataView: DataViewable { get } } public extension DataControllable { public func setupDataSource() { MyService.get { items in self.models = items self.dataView.reloadData() } } }
Instead of working with “self.tableView” or “self.collectionView“, my protocol is going to work with “self.dataView“, which is a “DataViewable” control that I created earlier. Even though it can either be a table or collection underneath, I can call “reloadData” on it, or any other relationship I’d like to bridge between tables and collections.
The table and collection view controllers would look something like this:
class TableViewController: UITableViewController, DataControllable { var models: [Contentable] = [] var dataView: DataViewable { return tableView } override func viewDidLoad() { super.viewDidLoad() setupDataSource() } } class CollectionViewController: UICollectionViewController, DataControllable { var models: [Contentable] { get set } var dataView: DataViewable { return collectionView! } override func viewDidLoad() { super.viewDidLoad() setupDataSource() } }
Notice by simply calling “setupDataSource” on either the table and collection view controllers, it gets populated and refreshed with the same logic.
What the NIB?!
There is another glaring inconsistency in the table and collection views: “registerNib“. For table views, the function signature is the first and the collection views is the bottom:
func registerNib(_ nib: UINib?, forCellReuseIdentifier identifier: String) // UTableView func registerNib(_ nib: UINib?, forCellWithReuseIdentifier identifier: String) // UICollectionView
Spot the difference?… “forCellReuseIdentifier” versus “forCellWithReuseIdentifier“. This almost looks like a typo, why the difference?! This makes it difficult for them to conform to the same protocol. So before we attempt to marry them, let’s reconcile the API by extending the table and collection views and add an identical “registerNib” signature, let’s also make it less burdensome while we’re at it:
public extension UITableView { public static var defaultCellIdentifier: String { return "Cell" } public func registerNib(nibName: String, cellIdentifier: String = defaultCellIdentifier, bundleIdentifier: String? = nil) { self.registerNib(UINib(nibName: nibName, bundle: bundleIdentifier != nil ? NSBundle(identifier: bundleIdentifier!) : nil), forCellReuseIdentifier: cellIdentifier) } } public extension UICollectionView { public static var defaultCellIdentifier: String { return "Cell" } public func registerNib(nibName: String, cellIdentifier: String = defaultCellIdentifier, bundleIdentifier: String? = nil) { self.registerNib(UINib(nibName: nibName, bundle: bundleIdentifier != nil ? NSBundle(identifier: bundleIdentifier!) : nil), forCellWithReuseIdentifier: cellIdentifier) } }
The “registerNib” signatures are now the same for tables and collections, but I’ve also made it more convenient by adding optional and default parameters, like for the cell identifier. Also, the function is accepting strings instead of forcing the end developer to create nib and bundle instances first to pass them in. I’ll let the function create instances underneath automatically. This way, I can simply do this:
self.dataView.registerNib("MyTableViewCell") self.dataView.registerNib("MyCollectionViewCell")
Sugar and Spice
For the finale, I’d like to tackle the dreaded “dequeueReusableCellWithIdentifier” API. The philosophy of Objective-C is to make everything as verbose as possible. On the other hand, the philosophy of Swift is to make everything as swift as possible. So for this archaic API, I’d like to convert it to a subscript. Makes sense that you’d retrieve cells out of a table using subscripts, doesn’t it?! Here it goes:
public extension UITableView { public static var defaultCellIdentifier: String { return "Cell" } public subscript(indexPath: NSIndexPath) -> UITableViewCell { return self.dequeueReusableCellWithIdentifier(UITableView.defaultCellIdentifier, forIndexPath: indexPath) } public subscript(indexPath: NSIndexPath, identifier: String) -> UITableViewCell { return self.dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath) } }
Now I can simply do this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView[indexPath] as! MyTableViewCell ... return cell }
Extracting the cell from the table using “tableView[indexPath]” instead of “tableView.dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath)” feels so much more natural. I’ve overloaded it too in case you want to use a different cell identifier.
Putting It All Together
We’ve made some breakthroughs with tables and collections in this post. Let’s summarize the evolution.
First is the “DataViewable” protocol to marry the “UITableView” and “UICollectionView” together:
public protocol DataViewable { func reloadData() func registerNib(nibName: String, cellIdentifier: String, bundleIdentifier: String?) } extension UITableView: DataViewable {} extension UICollectionView: DataViewable {}
Next is the “DataControllable” protocol to extend the “UITableViewController” and UICollectionViewController” simultaneously:
public protocol DataControllable: class { var models: [Contentable] { get set } var dataView: DataViewable { get } var cellNibName: String { get } } public extension DataControllable { public func setupInterface() { self.dataView.registerNib(cellNibName) } public func setupDataSource() { MyService.get { items in self.models = items self.dataView.reloadData() } } }
The “setupInterface” and “setupDataSource” functions is where you’d put your view and data retrieval logic. The table and collection views would both get reloaded too.
Finally, the table view controller would like this in the end:
class TableViewController: UITableViewController, DataControllable { let cellNibName = "TableViewCell" var models: [Contentable] = [] var dataView: DataViewable { return tableView } override func viewDidLoad() { super.viewDidLoad() setupInterface() setupDataSource() } override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return models.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView[indexPath] as! TableViewCell let model = models[indexPath.row] return cell.bind(model) } }
Similarly, the collection view controller as well:
class CollectionViewController: UICollectionViewController, DataControllable { let cellNibName = "CollectionViewCell" var models: [Contentable] = [] var dataView: DataViewable { return collectionView! } override func viewDidLoad() { super.viewDidLoad() setupInterface() setupDataSource() } override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 } override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return models.count } override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView[indexPath] as! CollectionViewCell let model = models[indexPath.row] return cell.bind(model) } }
It doesn’t seem so verbose anymore and the controllers feel much slimmer. To see a full working demo with these concepts in action, check out the GitHub repository. It contains the protocols discussed, as well as a framework with embedded NIB’s using the UIStackView so you can get a sense how this would work in the real world.
Happy Coding!!
You can even go further by adding bridging protocols extensions like this:
`extension DataControllable where Self : UITableViewController {
func var dataView: DataViewable {
return tableView
}
}
extension DataControllable where Self : UICollectionViewController {
func var dataView: DataViewable {
return collectionView
}
}`
Sweet, nice addition! Thx!!