In the first part of this architecture series, I wrote about how I decided to create a custom architecture for iOS apps because I was not quite satisfied with all the existing ones. Now’s the time to dive in and see how it evolved over the years.
The building blocks
As I already mentioned in part 1, these were the three main focus points for the first iteration of the architecture:
- Fetching data (from API, local database or basically anywhere)
- Presenting views (formatting models, adding colors, toggling states, etc.)
- Navigating from one screen to another
Why did I choose those 3 points? Well, in my experience from previous projects, the majority of code in ViewControllers would be fetching data, configuring the views with that data or simply doing some UI modifications which are not available in Interface Builder, or need to be adjusted at some later point.
And regarding the navigation, it just made sense to me that the ViewController shouldn’t need to know about other ViewControllers, or how the transition is done. That way the ViewControllers are more decoupled since they don’t need to know about each other directly.
The initial setup
So, let’s look at an example. If we are building an app that requires the user to log in, we will have a login screen. In our architecture, it will consist of three parts:
The LoginModelController is responsible for logging the user in and saving the user to local storage if the login is successful. The LoginViewController holds all the views (outlets) from the Interface Builder. It also handles user input and serves as a mediator between the ModelController and the Presenter. It gets the data from ModelController and passes it along to the Presenter which configures the views with that data and additional properties.
What about navigating from one screen to another? The ModelController has a reference to every service it needs, like a networking service that makes the API calls or a database service which saves and gets data from the database. It also holds a reference to NavigationService, which can create and navigate to another screen. It instantiates the ViewController from a storyboard or an XIB, connects the ModelController and Presenter, and injects the NavigationService.
Once the first project was done, it was time to analyze the impact of the new architecture and see what could be futher optimized. The first noticeable issue was a lot of ViewController boilerplate. The ViewController should be relatively simple and offload as much business logic as it can to the ModelController.
In our case, after getting the data from the ModelController, the ViewController then needed to decide what to do with the data (present it, save it to the database, handle errors…). It is way better that all of this is the responsibility of the ModelController and that the ViewController gets a notification only when it is needed. For example, if the result of an API call is just used for saving data to local storage, then the ViewController doesn’t need to be notified. But if a ModelController catches an error and decides this is the type of error that needs to be shown to the user, then it will notify the ViewController.
In the first couple of Coconut projects, the ModelController had direct access to networking and database services. This is not ideal for two reasons:
- Those services can get pretty big because every ModelController needs some functionality from them
- We cannot simply replace our database/networking implementation without changing the ModelControllers
That’s why we added a layer in between – Repositories and NetworkServices. A Repository holds all of the logic for adding, fetching and updating a specific model to any local storage. On the other hand, a NetworkService has the logic for CRUD operations of a specific server model. Services now have clearer responsibilities and are more decoupled and testable.
As I stated before, the NavigationService is responsible for creating new screens and showing them, but the overall flow of this was not convenient. For example, if you want to push a new controller to the navigation controller stack, you have to send the navigation controller instance through the ModelController to the NavigationService, so when it creates the screen, it can push it onto that navigation controller stack.
That’s why we created the Navigator object which holds an enum of where it can navigate to. Every Navigator is initialized with a Source that is a UIResponder and can be used to navigate to other screens. This means a Source can be a UIWindow, but it can also be a UIViewController.
So this is what the new Coconut hierarchy looks like. The controller is at the center because it controls how everything is connected and sets up communication between other objects. Other architectures like VIPER or MVP treat the ViewController as a View, but I prefer to treat it as a controller because it fits well with the entire Cocoa Touch framework.
The Coconut architecture went through many changes and I highlighted some of them in this article. This is an important thing to understand – as frameworks and APIs we work with change and more complex project challenges arise, you constantly need to improve and optimize your architecture. Find people who will challenge your previous decisions and make you look at things from a different angle. It is surprising how you can overlook even the simple stuff if you get used to it.
Stay tuned for part 3, where we will talk about the future of Coconut and how it will be impacted by SwiftUI. Oh, and if you’re still wondering why it’s called Coconut, well, we just thought it was a really cool name. 🙂
We’re available for partnerships and open for new projects. If you have an idea you’d like to discuss, share it with our team!