A Pattern To Tell Your Dad About
I wanted to come up with some clever twist on “measure twice, cut once” but then I realized saying the same thing a new way is the opposite of what this post is about. In programming, maximizing the reusability of code is a central concept. Sometimes, however, the tools meant to simplify our processes actually complicate them.
The Activity Feed Dilemma
Let’s say we’re implementing an activity feed. Not unlike other activity feeds, this one is designed to give the user a summary of the ways users and their connections were interacting in the app’s ecosystem. This information is displayed via a number of different cells that can be scrolled through.
Looking at the designs surfaced a common style across the cells, but when considering some of the finer details it became clear a single cell shouldn’t be responsible for the large variety of cell layouts. Creating a different XIB file for each cell gets around this problem, but requires repetitive UIView construction, meaning any future change to those elements will need to be made in each file, increasing the time spent and the possibility for error with each new cell type.
Constructing the views programmatically would allow me to have functions that implement the visual styling I want to reuse, all with a single point of control for future changes. But I find the programmatic construction of views to be very cumbersome compared to the interface builder.
Really, what I needed was an implementation that maximizes the number of unique XIB files and assembles them to meet my needs. With a container view, we can create a view once and insert it elsewhere in other views and view controllers. Simple container views are a nice way to break up dynamic or complicated view construction. Adding a layer of protocol conformation presented a solution that I felt would meet the demands of the activity feed. That is where protocol-driven container views come in.
A Shell For Your Cell
The first thing I wanted to do was create a base (or shell) container cell, which would be responsible for the elements of the cell that were most common to all cells: ActivityFeedShellTableViewCell.swift.
In ActivityFeedShellTableViewCell.swift I had access to modify the style of the common elements in one place. More importantly, this was the place to determine and instantiate the different types of bodies I wanted inside the shell cell:
configureForActivity takes an object that conforms to the ActivityFeedViewable protocol. ActivityFeedViewable is a protocol for the view models in our system. That way, in the shell’s configureForActivity we can confidently decorate the common elements based on the view model. Then, based on the type of our ActivityFeedViewable-conforming object, the shell instantiates the correct body view that conforms to ActivityFeedConfigurableBody. The configurable views are passed to addConfigureableView, which looks like this:
This method is pretty simple, and definitely takes advantage of the CocoaPod PureLayout. (Shoutout to: https://github.com/PureLayout/PureLayout) In one function call (autoPinEdgesToSuperviewEdges()) it takes a subview (the bodyView) and constrains it to be centered and pinned to the frame of its container (the bodyViewContainer). In this way, it effectively replaces the container view, while maintaining the constraints and positioning implemented in the XIB. The final step is to call the bodyView’s configureForActivity, passing it the view model it represents.
The Basic Body View
Now that we’ve covered the UI and logic behind the shell view, and the model for its body content, let’s tie that together to implement a body view. As an example, we’ll look at the ActivityFeedBasicBodyView.
It has all the features of our base shell (the corner radius, shadow, and side strip of color), as well as an icon, the activity type, its description, and a date. Because the shell cell’s body container view reaches the edges of its content, we will treat the creation of our base content view as if its own content was being constrained against the interior (or container view) of the shell body. This makes creating the view for the basic content very intuitive as we aren’t accounting for an extra padding or buffer. We effectively make it in the XIB as we would any cell.
And, because the shell cell has already had its components decorated by the model, configuring the basic body view with configureForActivity is very simple:
NOTE: the actual labels are configured in an activityContentView, which continues our container pattern because that format of content is used in many places.
As you can see, configuring this view is very straightforward and self-contained, while its parent view (our shell cell) can handle its own business without caring about the details of its body.
Because the configuration logic for each body view is self-contained, and the shell cell effectively dispatches which body should be used, this pattern makes for a very clean cellForRowAtIndexPath in our ActivityFeedViewController:
We simply dequeue a shell cell, and pass it the viewable (which is a ActivityFeedViewable) at indexPath.row in our collection of activities.
Beyond the Basic(s)
What is nice about this pattern is that, as long as new body views and models conform to the correct protocols, it requires no new work for the tableView delegate. New body views can be dramatically different in content and height, as they’re all nicely laid out with constraints in XIB that will take advantage of the auto layout.
By enforcing protocols on the view models and on the body views, I adapted the general use of container views to reinforce styling and reduce repetitive view construction and boiler-plate code. As “activity feed” style views are fairly common in app design, I think the conscious use of protocols in combination with container views provide substantial flexibility with reduced XIB overhead to achieve a family of views.