When building screens in an iOS app, a standard practice is build one viewController class per screen. What if the search and results page share a lot of code? What if they both contain an interactive header to select filter and sort options? What’s the best way to split this up?
For this post, a “screen” or “page” is defined non-technically to mean whatever a user sees in an app at a given moment. For instance, we might say an app user might be looking at the search screen before transitioning to the results page. This may translate to “the UserSearchViewController presented the SearchResultsViewController.”
Potential options:
- Copy paste method: copy-paste all the shared code into both ViewControllers. After all, MVC is called “Massive View Controller” for a reason.
- Top-down approach: create everything in a single ViewController, using flags and functions to show the user search controls and the search results when appropriate
- Object Oriented approach: Create a base class called “SearchBaseViewController” that encapsulates base functionality and let the UserSearch and SearchResults VCs be subclassed
- Functional approach: Create a protocol extension that defines common methods and behaviors, then declare both VCs as protocol conformant
- Bottom-up approach: Split as many shared pieces into standalone components or view “fragments” as possible
Which to choose? As an engineer, the short answer is it depends. Instead of using those frustrating, meaningless words to dismiss the question outright (I’m looking at you, Stack Overflow), here’s the guiding principals I’d use to make the decision.
Guiding Principles and TLAs
Inventing terminology and coining acronyms like “TLA” (Three-Letter-Acronym) to make basic concepts sound complex is a favorite past time of engineers. It also turns out to be shockingly helpful not just for mnemonics but for grok-level understanding and internalization of ideas. To that end, here are the most relevant principals in TLA form:
- DRY – don’t repeat yourself. Avoid duplicate code
- SRP – single responsibility principal. Avoid doing too much in one spot
- YAGNI – you ain’t gonna need it. Avoid over-engineering beyond current code needs
These ideas and terms have been advocated by well-renown engineers like Uncle Bob Martin for decades. While the nuances tends to change over time, the principals continue to work regardless of processes and tools used. Detailed breakdowns and the concepts, applications, and studies could fill a book, so I’ll focus here on how can we apply these principals to UI design in Swift.
Filtering options
Let’s break down each of the potential options and explore them in context of the guiding principals.
Copy-paste method
Copy-pasted code is often dismissed as a DRY violation and immediately dismissed. But I think it can be useful in very particular cases. Is the code for the first class complete, well-tested, and bug free? Yes!? Is anyone ever going to have to re-compile, re-visit, or troubleshoot the code? No!? In that case, go copy-paste to your heart’s delight and change all the commit headers to hide your shame. Because if you guessed wrong, and the code does have to be revisited, you’ve just created a mountain of technical debt for the next guy. Jerk.
Top-down approach
MVC doesn’t actually stand for Massive View Controller, but we shouldn’t let that stop us. By including precisely too much code, though, are we inherently violating the SRP? I guess that depends on how loosely and subjectively we define “single responsibility.” If we claim the Search VC’s responsibility is to “controls the Search screen,” technically we can ignore how big and complex that responsibility is. Your teammates, on the other hand…
I would argue that in Swift, a top-down approach using standard components should be the first step towards building a screen. Once pieces of that screen start to share behavior, look, and feel to other screens, only then should you try to group that common code and behavior into reusable modules. Until then, YAGNI.
One major pitfall to avoid is managing component *state* when possible. If you have a cell that displays a calendar date and short snippet from the first meeting, then it’s a great case for a reusable component. If clicking on that cell displays a larger, full breakdown of the day’s events, create another cell for it. Trying to make the cell responsible for knowing if it’s in the “unclicked’ vs the “clicked” state, having it responsible for expanding and collapsing itself, and keeping it hooked up too a superset of all the potential IBOutlets is a recipe for non-reusable, buggy components with way too much code. Keep it simple (KISS) and don’t be afraid to create new components for similar but non-shared functionality.
Object Oriented and Functional approaches
Swift is considered a hybrid language that supports both Object Oriented and Functional paradigms. In terms of grouping sharing code between classes, especially screen-related ones, the difference between the two is more about style and syntax than anything substantive.
That said, OO paradigms tend to be less favorited in Swift. Classes can only inherit from a single other class, so if classes A & B share code, and B & C share code, then it’s impossible to combine all the shared code using inheritance without including unnecessary overlap. I generally avoid creating Base classes for UIViewControllers because they tend to become universal dumping groups for all shared functionality, often leaving a lot of cruft and boilerplate that’s hard to remove once obsolete. Avoid extending UIViewControllers unless it’s to add application-wide, universal behaviors like logging and error handling.
Protocol extensions can be more flexible and precise than inheritance, since they can be applied individually to classes without much limitation. The downside to burying common code in protocols, especially for view controllers, is that you cannot define variables (including @IBOutlets), it’s not always easy to discover which Protocols are available, and it can be tricky to debug where protocol extension code is executing. I tend to only use protocol extensions in views for common, one-off functionality like displaying Error popups and little else.
Bottom-up approach
Given the number of small, reusable components included in UIKit, it can feel overwhelming to design and build the first a large, complex screen in an app from scratch. If you build these screens using a bottom-up approach, however, each subsequent screen should get easier and less complex. How?
Above, I advocated starting from a top-down approach. So your first screen may contain two-dozen outlets and just as many IBActions and helper functions to keep track of things. Do not prematurely optimize or over-engineer the screen on first pass! If you do, you’re guaranteed to violate the YAGNI principal. Instead, the first time, build it as simply as possible, make sure it works and is approved by stakeholders or users, and then consider it available for refactoring. Once you’re starting your second screen, look for small, common groups of components and behaviors. Instead of copy-pasting this stuff, turn it into reusable components, refactor the first VC to use them, then apply them to the second.
This way, you group common code as you come across it, instead of preemptively guessing what will be common and potentially burning a lot of time making a single-use component reusable. You also avoid having a rewrite a reusable component or tacking on a leaky abstraction that wasn’t originally part of that feature (perhaps violating SRP). And while the investment in generalizing components is not insignificant, it can being limited to small and known-reuse pieces, has a concrete goal for generalizations, and has proven at least some reusability.
Creating reusable components and fragments
The bottom-up approach to building screens advocates for reusable components, but what does this mean? One simple example is a Table View Cell. It doesn’t matter whether you create it as a prototype cell directly inside a story board, or design one as a standalone nib. It doesn’t matter how big it is either; the most basic UITableViewCell is nothing but two UILabels grouped horizontally (called title and description). If the smallest unit of reuse is two labels, then our reusable component should only be two labels. If it’s a complex cell with special interaction and display logic, then all that can be grouped together.
We’re not limited to TableViews and CollectionViews, though. We can create a reusable nib, which is simply a UIView, that contains nothing but two labels too. Or make it as complex as necessary. Keep in mind that if you’re going to bother making something its own component, it should do a single thing (SRP), and you should first have at least two locations that need to use it (DRY/YAGNI).
Does it matter if each screen is a combination of a few UIViews wrapped in a single UIViewController? Not really, so long as those views are reused elsewhere. Does it matter specifically where the breakdowns occur? Also, not really, but if you start smaller and build up, a project will because easier to develop in over time. This is generally true regardless of the technology and platform used. For example, see the similarities in this post: http://aem.matelli.org/componentizing-aem-content/, a guide to breaking down components in the Adobe Experience Manager web-based content management system.
Views vs ViewControllers
Some people prefer larger Storyboards over lots of individual nibs. I’m one of those people, but have to admit it doesn’t matter much. Reusable UIViews generally cannot be embedded into Storyboards. But reusable UIViewControllers *can* be. For that reason, I’ve seen developers wrap small controls into reusable UIViewControllers and lay them out in storyboards.
What’s the difference between a component that’s based on UIView vs one that’s a UIViewController? In practice, the difference is just what methods you hook into to manage initialization and display:
UIViewController
UIView
after init (once)
viewDidLoad
awakeFromNib
before display (each)
viewWillAppear
layoutSubviews
If we drill into memory footprint and general resource usage, we can dive into a discussion of why UIViews should be preferred over UIViewControllers the same way that struct is preferred over class. But unlike the struct vs class difference, there’s nothing fundamental different about either approach. Go to town!