Once upon a time, I wrote view controllers the size of Texas, thinking I was already clever for moving my table view data sources out of them. But over time, there was something poking my brain and heart saying, “Chief, there’s got to be a better way to write software. You need to up your game.”
It was then that fateful day in 2014 that Mutual Mobile published an article on something called VIPER. I skimmed the article, and promptly and stubbornly moved onward having not actually messed with VIPER.
Fast-forward to today
Over the years, I started to evolve my architecture choices. As many say, while there is no silver bullet, I now often find VIPER best suits my needs for applications with a clear direction and long-term vision. I truly love the modularity and how it makes the pain of unit testing in iOS go away (mostly).
On my journey to VIPER’ing, I hadn’t really come across thorough and clean examples of implementation inside a cross-platform application. Therefore, I recently decided to write Listy — a to-do list app for iOS, tvOS, watchOS, and macOS, and my first whole-app contribution to open source software 🎉. I’d love to share with you how I built it, and hope you find it helpful in your own software development adventures.
If you’re saying, “Look, that’s great, but I’d rather just check out the code,” here you go.
The inception of Listy came from a job interview take-home test. The purpose was to write a simple to-do list app which only creates and deletes lists and tasks, but spend the bulk of development time constructing a project foundation that would best serve scaling the app in future development — essentially laying down some rock-solid roots for a tiny tree to one day become a tall, beautiful one with many branches.
Unfortunately, we didn’t end up being a match, but I decided to continue development of Listy anyway for myself, and publish my findings in creating a cross-platform app with VIPER running underneath it. With a limited feature set, I wouldn’t manage my daily to-dos with Listy, but I think it makes for a pretty nifty model home/reference guide for development of other projects!
As with most iOS apps I build from scratch or refactor, the first thing I usually do is create a base Cocoa Touch framework to house primarily pure Swift and Foundation framework API interaction — custom data structures, extensions on Swift data structures, models, and data persistence — things we should be able to use and access in app extensions and anywhere else.
I then create a second Cocoa Touch framework for handling all common UI(Kit) interaction — common views (i.e. view controllers), table view data sources/delegates, UI data structure convenience extensions — things we can use across many different use cases.
Where does VIPER fit into this? Normally, my VIPER modules would be, in their entirety, in the iOS main app and extensions. With going cross-platform, where and how does it make sense to break apart the snake?
The foundation layer, or ListyKit
VIPER’s presenter and interactor shouldn’t even think about touching UIKit. The presenter should know when to present and update UI though (via the view and router), and the interactor should handle the business logic and CRUD’ing entities¹. Given how similar presenter and interactor behavior is for list and task management, I put them all in ListyKit, the base framework.²
With the presenters and interactors at the base of all the Listy apps, in any app, I can do neat things like:
- Fetch list and task data, with which I can update my lists and tasks views.
- Tell lists and tasks views to update themselves, delete table view rows, and show error alerts.
- Have the router prepare to show edit list and edit task views.
I also placed all router input and output protocols in ListyKit, but not any actual routers. I thought about including routers, but it would be going a bit overboard as routing is significantly different platform-to-platform. For example, when the user indicates to add a task, on iOS, we present a whole separate view, whereas on macOS, we add a table view row with a textfield to the list of tasks. You could use preprocessor conditionals to load different platform-specific code (and I do use a few elsewhere in the app), but felt things might get a bit too hairy in the router to do this. (And well, too many of these make things smelly.)
The (iOS and tvOS) UI layer, or ListyUI
Between three different devices, the iPhone, iPad, and Apple TV share a lot in common on the UI API front. Therefore, I had ListyUI sit on top of ListyKit, and offer the following:
- Set up, configure, and update all list and edit views.
- Reuse table view data source and cell configuration in all lists and tasks views.
- Share view routing output implementations (e.g. presenting/pushing/dismissing views).
- Present error alerts
In the updateView(…) functions of each view, I perform block-based setups for cell configuration, selection, and deletion. This allows me to separate the table view data sources and delegates and from the view, while still being able to call to the presenter as needed when a user takes action.
What about watchOS and macOS?
Well, they’re extra special, in that they have fancy things like WKInterfaceControllers and NSViewControllers, which our friends iOS and tvOS don’t have. While we could, in theory, create some crazy master abstraction of views across all platforms, I thought it best to just handle UI interaction separately for watchOS and macOS. After all, we still have the “IPE” in VIPER available from ListyKit, which takes care of a lot of other heavy lifting.
You mentioned cross-platform, yes?
I initially assumed I could make ListyKit and ListyUI work cross-platform out of the box, but nay. I quickly found cross-platform frameworks are no walk in the park. Luckily, I discovered this very nice guide that helped me understand how to share code via platform-specific frameworks touching a shared Info.plist file (setup details are a bit out of the scope of this article, but I recommend checking out that article to learn more).
I ended up setting up the following platform-specific frameworks:
- ListyKit iOS
- ListyKit tvOS
- ListyKit watchOS
- ListyKit macOS
- ListyUI iOS
- ListyUI tvOS
Each framework connects to the same code via sharing the aforementioned Info.plist path in Build Settings, and file target membership configuration. That is, each file still must be manually associated as needed with each platform framework. For example, ListsInteractor.swift must have a target membership of ListyKit iOS, ListyKit tvOS, ListyKit watchOS, and ListyKit macOS. If Apple launches a new augmented reality device, well, you might need to spend a few minutes check target membership boxes for ListyKit augmentedOS — not too bad.
I’m sure my VIPER implementation is not perfect across the board. If you see areas you think can be improved, I’m all ears. In fact, I welcome your contribution! I plan to follow up this VIPER-specific article with other articles on how I made Listy, covering the specifics of things like abstracting view routing output and presenting error alerts. Also, if you like and want to use Listy code, please take it and run free-ish with it (it still has a license, yo). Thanks for taking the time to read my post, and I hope you found it helpful. Feel free to reach out for questions/thoughts in the comments! Listy on GitHub
¹ Technically, one should not pass entities from the interactor back to the presenter, and create simple data structures associated with use cases. However, given the current entities in Listy are super small and simple, I decided to break this rule for now.
² One could argue presenters belong in ListyUI because they are UI-related functionality, despite not touching UIKit. While I agree, I felt it was a better trade-off to make them more easily reusable from ListyKit. If presentation logic for Listy becomes more complicated/platform-specific in the future, I will move them accordingly to individual app code.