List controllers, like UITableViewController
s, UICollectionViewController
s, and any custom controllers used to display lists, are extremely common in iOS. Throughout the years various techniques have been used to move logic away from List controllers, but it is still easy to give them too much responsibility. Enter protocol extensions! With Swift 2.0’s flagship feature, we can elegantly abstract logic out of our list controllers and into our protocol extensions. By the end of this talk, you will be able to keep your list controllers slim by taking advantage of Swift’s protocol-oriented nature.
Core Concepts (00:46)
List controllers (00:49)
A list view controller is a controller that handles tables, collections, or custom lists that you would have made for your own application. They are used all throughout iOS (if anyone has used iOS before, I can guarantee that you have hit a table view at some point, and that is a list controller). If you have ever developed for iOS, you have had to deal with data source methods and delegate methods that help populate the tables (e.g., tableView:cellForRowAtIndexPath:
). These controllers tend to get bloated. It is tempting to put all of your code in this controller (your view handling code, your cell handling code, and your model handling code… all in this controller). It is all nice and packaged, but we end up with a huge file (hard to maintain), and with code duplication. That is what we wanted to solve with protocol extensions.
Protocol extensions (01:40)
Protocol extensions are arguably the best Swift 2.0 feature (at least I think so). They allow you to extend your protocols to provide implementations for the properties and methods that you have defined in your protocols (in the same way that you can do that with a class, struct or enum in Swift). They are powerful and used throughout the Swift standard library.
Let’s take an example:
protocol StateMachine {
mutating func reset()
func next() -> Self?
func previous() -> Self?
}
We have a protocol, a StateMachine. You have three methods: reset()
, next()
and previous()
. A protocol is a contract: “if I want to be a StateMachine, I can be any object, but if I want to be a StateMachine, I have to implement reset, next and previous”. We can use that to our advantage: we can use that knowledge to extend it. In this case, we know that anyone who is a StateMachine will have reset()
, next()
and previous()
implemented for us.
extension StateMachine {
mutating func advance() {
if let next = next() {
self = next
} else {
self.reset()
}
}
mutating func reverse() {
if let previous = previous() {
self = previous
} else {
self.reset()
}
} }
In advance()
, we will go, “do I have a next state? If I do, let’s go to it. If I do not let’s reset”. We have used logic that is custom to the extension using the protocol methods that we know will be implemented by our conformer. We have next and previous (that has been implemented for us in our conformer); likewise for reset()
.
Protocol extensions are simple when you get the hand of it. They are powerful, and that is what is going to power this approach of protocol extension with List View Controllers.
Previous Abstraction Technique (03:16)
How did we use to make lighter view controllers before we had protocol extensions?
Most of the community (myself included) used abstraction objects. They were classes that would conform to the table delegate and the data source of the table view. It will also take the model of your list (e.g., an array). It will keep that code abstracted away from the rest of your app, and you can have a lighter view controller.
That is in line with the MVC model (model view controllers). We used to have a big list view controller. And after the abstraction object, we had a lighter list view controller (with our logic abstracted away in this fancy abstraction object). If you have another list view controller that shares the same data (or has a very similar way of handling the data), we could just point to the same abstraction object and avoid code duplication across our app.
This method is still a good method. The protocol extension approach does not negate this. In fact, if your app is complex, it might be a good idea to use them side by side.
Lighter Controllers With Protocol Extensions (04:30)
Because Swift is a protocol-oriented language, we are going to start thinking in terms of protocol first. Before we even think of classes or anything else, let’s think of protocols.
List
protocol (04:40)
What makes a list is we need to have the identification of the cells in the list. We need to know how to configure those cells, and how to react when one of these cells get tapped.
public protocol List {
associatedtype ListView
associatedtype Cell
associatedtype Object
func cellIdentifierForIndexPath(indexPath: NSIndexPath) -> String
func listView(listView: ListView, configureCell cell: Cell,
withObject object: Object,
atIndexPath indexPath: NSIndexPath)
func listView(listView: ListView, didSelectObject object: Object,
atIndexPath indexPath: NSIndexPath)
}
We have a protocol with three properties that are associated types: ListView
, Cell
, and Object
. We are using associated types because we do not know what List type we are dealing with (e.g. a table, a collection, or one of your own classes). You want to be as generic as possible.
We have three methods: the identification of the cell, what we are going to do once the cell needs to be configured, and what happens when the cell is tapped. Coming from List
, we can be more specific. We can inherit from List
and create this NonFetchedList
protocol. In this example (see below) we are going to use a table that is using a static data source, an array of sections and rows (to keep it simple). The paradigm of using protocol extensions remains the same for more complex examples.
NonFetchedList
protocol (05:38)
public protocol NonFetchedList: List {
var listData: [[Object]]! { get set }
}
public extension NonFetchedList {
var numberOfSections: Int { ... }
func numberOfRowsInSection(section: Int) -> Int { ... }
func objectAtIndexPath(indexPath: NSIndexPath) -> Object? { ... }
func isValidIndexPath(indexPath: NSIndexPath) -> Bool { ... }
}
A protocol of NonFetchedList
requires a property that is listData
that is an array of your sections and rows. From that we can start extending our protocol. We know what object we can have and an indexPath
. We have enough information to deduce whether it is a valid indexPath. And the cool stuff is the conformer. Your UITableViewController
subclass, for example, can have that for free. All they have to do is conform to NonFetchedList
protocol. You can call into any of these methods, which have been implemented and abstracted away in the protocol extensions.
TableList
protocol (06:46)
We are going to deal with a table. Let’s build on the NonFetchedList
protocol and inherit from that (again). We are going to conform to the data source and the delegate of UITableView
. That sounds like abstraction object… and it is; it is meant to be similar (because it is a great model). But there is a couple of benefits that we get from protocol extensions:
-
We get the compiler safety. We cannot get our code to compile if we do not conform to all of the
TableList
protocol requirements. That means that there are no runtime errors (that we will get with an abstraction object). Let’s say that you forgot to give yourtableView
a number of rows; with theTableList
protocol, the compiler will help you and tell you: “I am not going to let you compile. You have not given me enough information”. That is safety, and that is at the core of Swift. -
You do not have to deal with an object that you have to pass or copy around. You have that logic in your protocol extensions. All you have to do is conform to
TableList
. None of: copying, managing where your objects are, what instance is going where over that abstraction object. That is simpler.
Let’s have a look at the TableList
protocol (it is anticlimactic). We need a tableView to find out what the list view is. All that implementation (that you would normally have in your abstraction object) is in the extension.
public protocol TableList: NonFetchedList, UITableViewDataSource, UITableViewDelegate {
var tableView: UITableView! { get set }
}
TableList
protocol - recap (08:27)
We started off with a List
protocol. That list protocol had a NonFetchedList
protocol that was inherited from it. From that, we were able to deduce cool and useful extension methods that anyone who conforms to the NonFetchedList
protocol gets for free. We load that again, we have a TableList
protocol. That conforms to the delegate and the data source of UITableView. Now, we are able to have enough information to provide extension methods for our TableList
. We are keeping it simple. In this case, all we need to care about is the tableCellAtIndexPath:
, and what happens when you select it.
TableList
Protocol Extension (09:08)
public extension TableList where ListView == UITableView, Cell == UITableViewCell {
func tableCellAtIndexPath(indexPath: NSIndexPath) -> UITableViewCell {
let identifier = cellIdentifierForIndexPath(indexPath)
let cell = tableView.dequeueReusableCellWithIdentifier(identifier,
forIndexPath: indexPath)
if let object = objectAtIndexPath(indexPath) {
listView(tableView, configureCell: cell, withObject: object, atIndexPath: indexPath)
}
return cell
}
func tableDidSelectItemAtIndexPath(indexPath: NSIndexPath) {
if let object = objectAtIndexPath(indexPath) {
listView(tableView, didSelectObject: object, atIndexPath: indexPath)
}
}
}
We want to be more specific, less generic. We know we are dealing with a tableview. Our ListView
can now become a UITableView
and we can tell the compiler that our cell is now a UITableViewCell
. Whenever anyone implements a protocol that is TableList
, they can interact with table view objects without typecasting from UIView
to UITableView
or AnyObject
. Then we have to conform to the UITableViewDataSource
. We are going to create the implementations (that can be called from anywhere) in order to keep that ball of paid code away from your list view controllers, keep that into the protocol extensions and keep that abstracted.
From the delegate & data source of UITableView
(09:48)
We are going to use a method that has been declared in the protocol extension of our protocol parent (similar to Protocol Extension “inception”?…not a very good name). We can use objectAtIndexPath
. We know that it has been provided for us by our conforming objects (our conformer). We can deduce that we will get an object (without even knowing how the implementation happens) through protocol extension.
Lastly, we will use List
to get the identification of our cell. We will set up that cell for our users (or whoever the conformer is): “I am done setting that up, here is the object that I have. I need you to configure it now. And I need you to react to when this cell has been tapped”.
Through a series of protocol extensions, we were able to set up a table that it is safe with the compiler. The compiler can help us. We do not have to pass an object around. To get a slew of features for free through protocol extensions, we have to conform to five things: the tableView
(that you get for free in the UITableView
controller); your models (your array of sections and rows); and the identification, configuration, and our reaction to a cell tap.
Can We Do Better? (11:31)
Static cells (static table views and static list view controllers) were cool back in 2008 and 2009, when iOS3 came out and iOS2 was the hot stuff. Now we have Core Data–backed table views. Users are used to our table views and list view controllers being dynamically updated right in front of their eyes. How can we do that using that same paradigm?
FetchedList
protocol (11:55)
We can do better, without adding much complexity (if any) on our conformer: instead of inheriting from List and creating a NonFetchedList
protocol, we can create a FetchedList
protocol. And this is backed by an NSFetchedResultsController!
. The NSFetchedResultsController
will allow us to keep track of what is happening in your core database (or whatever you might want to point it to). That allows for the cells to dynamically update.
public protocol FetchedList: List, NSFetchedResultsControllerDelegate {
var fetchedResultsController: NSFetchedResultsController! { get set }
}
public extension FetchedList {
var numberOfSections: Int { ... }
var sectionIndexTitles: [AnyObject]? { ... }
func numberOfRowsInSection(section: Int) -> Int { ... }
func isValidIndexPath(indexPath: NSIndexPath) -> Bool { ... }
func objectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? { ... }
func titleForHeaderInSection(section: Int) -> String? { ... }
}
FetchedTableList
protocol (12:26)
Instead of getting a List
data that is static, we will get a NSFetchedResultsController
. From that we can deduce cool methods (e.g., the title for the sections on this indexPath). We are going to subclass the FetchedList
protocol with the FetchedTableList
protocol, and ask for a table view again. But you can see that the implementation of the extension is slightly more complicated: we have to deal with the table view and the NSFetchedResultsController
delegate methods (that will handle all the fancy automation of the cells being inserted and deleted).
public protocol FetchedTableList: FetchedList, UITableViewDataSource, UITableViewDelegate {
var tableView: UITableView! { get set }
}
public extension FetchedTableList where ListView == UITableView, Cell == UITableViewCell,
Object == AnyObject {
func tableCellAtIndexPath(indexPath: NSIndexPath) -> UITableViewCell { ... }
func tableDidSelectItemAtIndexPath(indexPath: NSIndexPath) { ... }
}
public extension FetchedTableList where ListView == UITableView, Cell == UITableViewCell,
Object == AnyObject {
func tableWillChangeContent() { ... }
func tableDidChangeSection(sectionIndex: Int,
withChangeType type: NSFetchedResultsChangeType) { ... }
func tableDidChangeObjectAtIndexPath(indexPath: NSIndexPath?,
withChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath?) { ... }
func tableDidChangeContent() { ... }
}
All of that complexity has been put in a protocol extension. Our users (or conformers, or subclasses of UITableView
or UICollectionViewController
), instead of giving us a static list data, have to give us a NSFetchedResultsController
. The barrier of entry to have more complexity in your table views and list view controllers is reduced. You just have to give us the NSFetchedResultsController
. That logic is abstracted in a protocol extension that is checked by the compiler to make sure it is safe. That is powerful because it is extendable. You can use it in your apps to keep your logic well maintained (easy to maintain your code).
Collection View Controllers (13:52)
We have not touched on collection views or custom list view controllers: How would they fit in that paradigm?
Approach Overview (14:06)
We had List
at the top of the paradigm, and two things that inherited from it: NonFetchedList
and FetchedList
. Inheriting from these two are: TableList
and FetchedTableList
. But there is also CollectionList
and FetchedCollectionList
that have been implemented. (Anyone who has implemented a collection view controller with NSFetchedResultsController
knows that it takes more than just a smile to get it working. 😅) It is cool that you can conform to the FetchedCollectionView
protocol and get functionality for free. But the power of this approach is that, if you have custom lists (or if you think that something could be done better), you can hook in anywhere you want. If you choose to inherit from List
, and you think my NonFetchedList
and FetchedList
implementations are garbage, you can create your own. If you think that TableList
was done well, but there are not enough features, you can extend TableList
. You can even inherit from TableList
, and do your own stuff. By that same logic, if you think that FetchedTableController
s could do with some loving, you could do your own. And you can hook in anywhere you want. That is scalable and highly customizable.
Pain Points (15:23)
We would not be good engineers if we did not admit that any approach did not have any pain points. In this case, you may have observed that instead of using tableView:cellForRowAtIndexPath
, my protocol extensions used a method called tableCellAtIndexPath:
. That is because of the way method dispatching works with protocol extensions. Briefly, because the tableView:cellForRowAtIndexPath:
has been implemented in the parent of your conformer class, it will use that as opposed to the protocol extension method. It means that if you are going to conform to FetchedTableView
or TableViewList
then you need to wrap your tableView:cellForRowAtIndexPath
calls and just call into the protocol extension. It does not add any complexity to your methods. It just means that there is more boiler plate code. Second, it is not Objective-C compatible (which breaks my heart). But, because we have generics, and it is protocol- & extension-heavy, we have to leave that behind.
More Information (16:48)
- The implementation of this paradigm is open sourced under the MIT license. If you want to play with it or if you think there are better ways to do things, I am all ears. Especially if you think you can get around the method dispatching with protocol extensions.
- The Swift language repo: Swift was open sourced in December last year. Useful for anyone who is into Swift. If you want to know how something is implemented, I highly recommend going there and finding out how a compiler does things (that is how I found out nasty things with protocol extension and method dispatching).
- Cool article: encapsulates and presents how method dispatching and protocol extensions work. Really fascinating.
questions?.filter({jad.canAnswer($0)})
(17:44)
Q: Do you anticipate any problems with Swift 3 or 2.2? Are there any language issues that may throw a monkey wrench in this in the near future?
Jad: I have compiled that with Swift 3 and 3.2 and it works fine. The whole point is that it is meant to be as portable as possible. So far I have not encountered any issues. The good thing is, because it is heavy on relying on list view controllers that already exist in the Objective-C world, it is nice because Apple has to support it too. If Apple decides to do that themselves, then they will hurt themselves first before it hurts me.
Q: Great talk, I really like the concepts. Two questions, one more general. Protocol extensions: I know currently there is no decent way to override methods, declare them extensions. Is there any way to work around that?
Jad: Great question. Currently, not that I know of. You would have to do a similar nasty trick that I did here. You would have to rename your methods. And that again has to do with how protocol extensions are implemented internally. But I am sure there is smart people at Apple that are on this. In fact in Swift 3, they are adding generic associated types. Yes, you have seen that too. I am sure that is going to uncover bugs. Hopefully they will do something cool for us.
Q: And a more applicable question. I understood that it works well when you have, let’s say one type of the cell, one type of the data. For example, settings app, the first screen. What if you have table view that has mixed content, and depending on your internal business logic?
Jad: Here we have an associatedtype
that is an object. This can be anything. If you have, let’s say (which does not happen that often), with table views. When you think about it, it is a list of objects that are similar. But if you do have different objects, what you can do to get around that, (I guess) you can create a wrapper object. Because this is an object that is generic. It can be an object that has its own logic to figure out what it should display or what object you should send back when you need to give it at a separate indexPath. If you have the first row in your tableView, this object could take it over and be, “I have an indexPath here. And as you can see, list view with objectAtIndexPath
, you have plenty of information to go on”. And you can be, cool, “I am the wrapper, I decide what I am going to show you right now”. But you are right. This is definitely a method which is highly optimized for lists that are most common (e.g., a common object type).
Receive news and updates from Realm straight to your inbox