神刀安全网

Protocol-Oriented TableView and CollectionView in Swift

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 {     funcreloadData() }   extensionUITableView: DataViewable {} extensionUICollectionView: 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.

Protocol-Oriented TableView and CollectionView in Swift

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 protocolDataControllable: class {     varmodels: [Contentable] { getset }     vardataView: DataViewable { get } }   public extension DataControllable {          public funcsetupDataSource() {         MyService.get { itemsin             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:

classTableViewController: UITableViewController, DataControllable {       varmodels: [Contentable] = []       vardataView: DataViewable {         return tableView     }       overridefuncviewDidLoad() {         super.viewDidLoad()         setupDataSource()     } }   classCollectionViewController: UICollectionViewController, DataControllable {       varmodels: [Contentable] { getset }       vardataView: DataViewable {         return collectionView!     }       overridefuncviewDidLoad() {         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:

funcregisterNib(_nib: UINib?, forCellReuseIdentifieridentifier: String) // UTableView funcregisterNib(_nib: UINib?, forCellWithReuseIdentifieridentifier: 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 vardefaultCellIdentifier: String {         return "Cell"     }          public funcregisterNib(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 vardefaultCellIdentifier: String {         return "Cell"     }       public funcregisterNib(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 vardefaultCellIdentifier: 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:

overridefunctableView(tableView: UITableView, cellForRowAtIndexPathindexPath: NSIndexPath) -> UITableViewCell {     letcell = 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 {     funcreloadData()     funcregisterNib(nibName: String, cellIdentifier: String, bundleIdentifier: String?) }   extensionUITableView: DataViewable {} extensionUICollectionView: DataViewable {} 

Next is the “ DataControllable ” protocol to extend the “ UITableViewController ” and UICollectionViewController ” simultaneously:

public protocolDataControllable: class {     varmodels: [Contentable] { getset }     vardataView: DataViewable { get }     varcellNibName: String { get } }   public extension DataControllable {          public funcsetupInterface() {         self.dataView.registerNib(cellNibName)     }          public funcsetupDataSource() {         MyService.get { itemsin             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:

classTableViewController: UITableViewController, DataControllable {       letcellNibName = "TableViewCell"     varmodels: [Contentable] = []          vardataView: DataViewable {         return tableView     }       overridefuncviewDidLoad() {         super.viewDidLoad()         setupInterface()         setupDataSource()     }          overridefuncnumberOfSectionsInTableView(tableView: UITableView) -> Int {         return 1     }          overridefunctableView(tableView: UITableView, numberOfRowsInSectionsection: Int) -> Int {         return models.count     }          overridefunctableView(tableView: UITableView, cellForRowAtIndexPathindexPath: NSIndexPath) -> UITableViewCell {         letcell = tableView[indexPath] as! TableViewCell         letmodel = models[indexPath.row]         return cell.bind(model)     }   } 

Similarly, the collection view controller as well:

classCollectionViewController: UICollectionViewController, DataControllable {       letcellNibName = "CollectionViewCell"     varmodels: [Contentable] = []          vardataView: DataViewable {         return collectionView!     }       overridefuncviewDidLoad() {         super.viewDidLoad()         setupInterface()         setupDataSource()     }       overridefuncnumberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {         return 1     }       overridefunccollectionView(collectionView: UICollectionView, numberOfItemsInSectionsection: Int) -> Int {         return models.count     }       overridefunccollectionView(collectionView: UICollectionView, cellForItemAtIndexPathindexPath: NSIndexPath) -> UICollectionViewCell {         letcell = collectionView[indexPath] as! CollectionViewCell         letmodel = 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.

Protocol-Oriented TableView and CollectionView in Swift

Protocol-Oriented TableView and CollectionView in Swift

Happy Coding!!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Protocol-Oriented TableView and CollectionView in Swift

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址