Brent Simmons wrote a great defense of his proposed “Cocoa Sin” of “Passing model objects to a UITableViewCell subclass”, expanding on the one apparently contentious item in his great list of 20 sins. Go read both posts, he’s so completely right.
Except I disagree. I’ve been down a number of
UITableViewCell paths that bit me in the end, many of which Brent seems to have experienced as well, but I have ended up settling on creating
UITableViewCell subclasses which take the model object they’re intended to display and break out the properties to the individual subviews so the controller doesn’t have to.
At its core, I’m taking code Brent would write like this:
- (void) updateCell:(MyTableViewCell*)cell
cell.nameLabel.text = object.name;
cell.valueLabel.text = object.value;
out of the controller and moving it to the cell as
- (void) updateForModelObject:(Model*)object
self.nameLabel.text = object.name;
self.valueLabel.text = object.value;
(Yes, he defines it as
-updateCell:forModelObject: can always be refactored out of that once the object is found for the indexPath.)
There may be some additional smarts in there: The color of valueLabel may get changed at a threshold value, the name text may need some truncation or transformation, an icon may change from sun to moon depending on a timestamp in the object. I’ve left them out of the example for simplicity.
At this simplistic level, they look identical. Yet I still feel putting the logic in the cell is superior. Why?
Look a bit closer at
-updateCell:forModelObject:. You may not even have noticed it, but there’s a distinct absence of the keyword
self. This isn’t encapsulation; this method is really just a utility method, which could just as easily be written in C:
void UpdateCellForModelObject( MyTableViewCell* cell, ModelObject* object );
Why is this bad? It throws away polymorphism, one of my favorite aspects of object-oriented programming. Let’s start with a theming example: We want to allow the user to chose between a set of differently-styled themes in which to display this homogeneous set of data.
This is fairly simple if it’s limited to colors, fonts, and other style-only changes in the themes—change the style in
-tableView:cellForRowAtIndexPath: and continue as before. What if one theme “FirstLast” should display my name as “Paul Goracke” and theme “LastFirst” should display as “Goracke, Paul”? The table view controller needs to know this difference. It’s an implementation detail of the theme, but under this design the controller doesn’t just need to know about it—it needs to implement it.
It will need to implement the quirks of every theme supported. Not bad enough for you? How about this: It also needs to determine which set of theme quirks to use each and every invocation of
-updateCell:forModelObject:. Why would you want to do this? I don’t.
By putting the logic in the cell, you only check the theme in
- Dequeue or create a cell of the appropriate type
-[cell updateForModelObject:] and the receiver cell will update as appropriate for the single theme it implements.
- There is no Step 3.
This is the glory of polymorphism: No matter how many crazy themes I make, the table view controller only needs to know which class to make for the specified theme because the subclass knows its specific implementation. If I decide to sunset a theme, I don’t have to worry about code cruft hanging about in the controller to handle that obsolete theme.
Polymorphism with Heterogeneous Model Objects
A heterogeneous data set in and of itself wouldn’t be a problem as long as all objects provide the core set of properties the cell requires to display the items uniformly. (Such would be the case with Brent’s Twitter app example.) Your cell(s) now take
-updateForModelObject:(id<ModelObjectProtocol>), they don’t care which particular protocol implementor you pass them as long as they provide the properties needed to display them, and you move on.
But what of a heteregeneous set where each object type needs to display differently?
Once again, the logic of which cell subclass to create lies with
-tableView:cellForRowAtindexPath:. After that, updating the cell’s display of model object property changes is handled by the cell. If the model object at that indexPath were to change type, call
-reloadRowsAtIndexPaths:withRowAnimation: on the table view to have it ask for a new or dequeued cell of the type appropriate for the model object’s new type.
What Makes a Controller a Controller?
Unfortunately, Apple seems to have unintentionally muddied the MVC waters by naming their main controller class
UIViewController. Many developers now have an extreme view of a controller: If it touches both a view and model object at all it needs to be a
UIViewController. Nothing could be further from the truth. Unless you need lazy view loading and view appear/disappear lifecycle management, creating a
UIViewController just overcomplicates matters.
Let’s look at Apple’s schematic of MVC, even though it should already be burned into every Cocoa and iOS developer’s brain:
It’s easy to get caught up in defining these objects based on their positions in the diagram, but what really defines them is their role within the system and how updates propagate. Keep in mind that these arrows are not object references, but interactions via updates and notifications—think of them as IBActions more than IBOutlets.
UITableViewCell subclass accepts a model object parameter and updates its constituent subviews as I have described, it is behaving as a data transformer, not a controller. It does not care about any future updates to the model unless the controller tells it to transform the updated model object, or to transform a completely different model instance.
Keys to Success
- Avoid the temptation to bypass the controller and start key-value observing (notifications are also verboten) on the received model object. This is the hubris that will lead to your MVC downfall (been there, done that). Leave all of the updating logic to the “true” view controller, and your cell subclass will remain a happy and healthy data transformer.
- Don’t even keep a reference to the received model object. Not only will this help avoid the KVO temptation, but it will encourage you to quickly move the property values into the appropriate subviews and let them take care of drawing, caching and refreshing.
- You’re a UITableViewCell—you’re displaying only one in a possibly innumerable collection of model objects. Leave the handling of the set of model objects to your
UIViewController<UITableViewDataSource> which should be invoking tableView updates based on responding to
UIFetchedResultsController or KVO, but there may be other update logic involved. That is, after all, the purpose of a controller.
- Keep your cells stupid. Brent’s examples of loading image or web service data asynchronously is still something to be avoided in the cell.
- Keep your cells simple. Many developers try to handle all possible configurations of a cell in one. They have numerous views which are hidden or displayed based on some model object criteria, which leaves a number of unused outlets lying around; this becomes confusing and hard to maintain. Using polymorphic cell subclasses, you can dedicate one cell subclass to each distinct configuration, avoiding unused views and code maintenance overhead.
There is absolutely nothing wrong with Brent’s minimalist guidelines. They will serve you well in many situations. But once your code base gets more complicated, I think you’ll be better served by not fearing better encapsulation of behaviors. Passing a model object to your cell is really only a venial sin.