Have you heard the term Massive View Controllers? It’s a term frequently used in the iOS dev community that refers to large view controllers problem that is faced by developers because of the way Apple enforces to some extent and guides developers to build iOS applications. View Controllers become too large due to the amount of responsibilities it has and the duties it must carry out.
Today I bring to you two of my favourite patterns that I have used to resolve Massive View Controller problem.
I have chosen the Model-View-ViewModel pattern and the Coordinator pattern as a method, when combined, to solve a number of issues with Apple’s MVC. I have bent these patterns a little bit to my style and might not be adherent to the others definition of these patterns.
This tutorial is not about explaining these two patterns in depth but rather show in code how the combination of both can provide a powerful solution for making good software.
Before I dive into patterns, let me summarise the need of having good software architecture. There is plethora of definitions out there that define good software and the need of good software architecture. My take, in short, on why to have good software architecture is to abstract some code by behaviour to aid common understanding between those working on the project or to have consistent software structure that allows for a mental visualisation of the software before it is written; I have found that a mental visualisation of the software structure graph helps me towards speeding test driven development.
There is plenty of material lightly covered in the implementation process of this tutorial but here is list of some of the material. Familiarisation of the following is recommended:
- Coordinator pattern.
- iOS Development
- XCode 8.x
- Swift 3.x
- Interface Builder
Show me the code! Or if you’d prefer:
Show me how its done!
In this tutorial we will be creating a single view iOS application that will retrieve a list of movies and display it. First we will implement Model-View-ViewModel into a ViewController followed by Coordinators to manage that ViewController, which will finally leads us to wiring it all up.
All right straight into coding. Go ahead and create a new project and choose single view application template. I am just calling it Movies-Coordinators.
First up: let’s delete the Main.storyboard file, ViewController.swift and remove the Main storyboard file base name key from the Info.plist file. Why? We are going to make use of Xib’s in this tutorial.
I believe the power of Coordinators is really shown by the ability to instantiate a ViewController with its expected dependencies. Xib’s allows us to do so (alternatively achievable by creating a ViewController programmatically) whereas Storyboards do not. Furthermore it enables us to test ViewControllers by applying inversion of control. Rather than the ViewController doing a discovery of the dependents objects or creation, our ViewController will declare its dependencies as part of the initialisation process. Every time a new dependency is declared in the init method the test target will no longer compile until the required changes are made. But testing is beyond the scope of this tutorial.
Creating the Movies View
Create a new Cocoa Touch class and along with it create an associated Xib file. Click on File > New > File… or just hit the cmd + N keys. Select Cocoa Touch class and then next. Let’s call our Xib and Swift files MoviesViewController. Make sure to subclass UIViewController. Tick Also create XIB file. Set Language to Swift. Click Next.
By now you should be able to see two newly created files. One called MoviesViewController.swift and another one called MoviesViewController.xib. Let’s have a look into our MoviesViewController.xib file. Click on the file and once loaded drop in a UITableView. Make sure that the table view takes up the whole screen – right at the bottom of the XIB view click on the add new constraints such that the UITableView fills the whole screen.
Next create a @IBOutlet reference to the table view in the view controller.
We will be using the table view reference to set the UITableView’s datasource so it can it display our movies list.
The view model
The view model will represent the state of our MoviesViewController. We will now be implementing it.
Create a new swift file and name it MoviesViewModel (File > New > File… or just hit ⌘N).
Insert the following code to your newly created MoviesViewModel.swift file:
There are two view models in the code above: MoviesViewModel, the ViewModel for the corresponding views state of the MoviesViewController and the MovieCellViewModel which will hold the state for the cell views of the table view.
We have not yet created the UITableViewDataSource. We could just make our view controller become the data source for the UITableView. But remember we are trying to take responsibilities away from the View Controller as much as possible. So we will create a dedicated class to manage the data source of our table view. The UITableView data source will be managed by the MoviesViewController. The MoviesViewModel is also managed by the MoviesViewController. The MoviesViewController’s responsibility will be to bind the state to the view, but the View Controller will have no notion of the underlying cell view of the table view. Therefore it is just a question of passing the CellViewModel to the manager of the cell which will be our UITableViewDataSource.
The table view data source
Let’s create a new class that will manage the data source for our UITableView and conform to UITableViewDataSource. Go ahead and create a new Swift file and name it MoviesTableViewDataSource for simplicity 😅.
MoviesTableViewDataSource will be straight forward. We will only implement the two required functions from the UITableViewDataSource protocol. Copy and paste the following code into your empty file:
The first function will just be returning the number of cells required in the table view which will be the number of entries in our array of MovieCellViewModel. The second function will be binding the state with the view. Whenever we want to update our view we will have to update our array of MovieCellViewModel and call reloadData() function of our table view.
Wiring View Controller, View Model and Data Source
For the next section we will make our MoviesViewController able to receive ViewModels. Upon receiving a new instance of the ViewModel, the view controller will update the table view’s data source with the new array of MovieCellViewModel and tell the table view to reload its views. Copy and paste the following two functions into the MoviesViewController:
In the code above we separate the binding of the view model from the setter function. We want to be using the binding logic in the viewDidLoad function of the UIViewController as we might set the viewModel before the view controller is is fully loaded. Therefore we have to wait for the view to be loaded first otherwise the table view property won’t be set and our app will crash trying to access a nil value when a UITableView instance was expected. On the setter function we save a reference to the latest view model, if the view is loaded then we can call bind otherwise we allow the viewDidLoad function to take the latest view model and bind it when the views are ready. Replace the viewDidLoad function with the following code:
So far we have laid down the classes that will be managing our views, we have understood what we will be presenting and implemented the code to do just that. The structure of our MoviesViewController looks like the following:
We now have to retrieve the movies we would like to display. We will present the title of the movies followed by the year in brackets as the main label in the cell. The details label will present the genres of the movie, comma separated.
We will now create our MoviesViewCoordinator. The coordinator will be responsible for presenting the view controller. It will also be required in gathering the necessary data for the views. Furthermore our movies data structure won’t come in the format we need to present, therefore the coordinator will also be required to do the transformation of such data into the expected format of the MoviesViewModel. Once the data has been transformed, the coordinator must pass the MoviesViewModel to the MoviesViewController.
Let start by creating a new file and naming it MoviesViewCoordinator. The coordinator will take a UINavigationController as the first dependency (make sure to import UIKit). We will be pushing an instance of MoviesViewController to the navigation controller. Our file should look like the following code:
Now add a new function called start that takes no parameter and returns Void (you don’t have explicitly return Void as this is the default of all functions). This is where our coordinator will create a new instance of MoviesViewController and pushing it to the instance of UINavigationController.
We will also be calling to fetch our movies from the start function. In a real world scenario this would be an asynchronous call and therefore it is possible we won’t receive our data before the screen has loaded. This will be depending on the internet connection. Our fetch movies function will take a closure that will take an array of Movies and return nothing. In this closure we will transforming the movies into MoviesViewModel and then passing the ViewModel to the ViewController.
To implement the above we will have to create an object that represents a Movie. Let’s create a struct that will represent a Movie. It will have 3 properties; title as a string, year as a string and genre as an array of strings. Create a new file and call it Movie. It should look like the following:
Back to the MoviesViewCoordinator. We left off at the point where we required the coordinator to fetch movies through a function that would take a closure upon fetching the movies. Let’s add the function to the coordinator. For now lets make the function’s body just call the closure with an array with a single movie. The movie will be an example just to see our code working. The fetchMovies function body could be replaced with a call to a repository for example.
By looking at the the Movie instance we expect the “Movie title (2017)” to be printed in the main label of the cell and “Action, Adventure” for the details label.
Now we need to transform or convert a Movie instance into a MoviesViewModel instance. It would be ideal to have a dedicated class that could be unit tested to convert, but for simplicity we will just add it to our coordinator as another private function. Copy and paste the following code:
Let’s make our start function call fetchMovies and the use the convert function to convert the result of the fetch within the closure. Lastly we’ll make it call the MoviesViewController’s set(viewModel:_) function with the result of the conversion. The MoviesViewCoordinator should now look like the following code:
We’re getting close to the end of this tutorial. Let’s get the app running with one final coordinator. The AppCoordinator.
Create a new file and call it AppCoordinator. This will be the root entry point to our application. The AppCoordinator is the best place to decide which screen to present based on the application state. For example, depending on whether the user is logged in we will present either the login screen or the main screen. The app is very simple though and the responsibility in this case of the AppCoordinator will just be to present our MoviesCoordinator. The coordinator will be created at the AppDelegate, which will call the the start function on it. The only requirement the AppCoordinator will need is an instance of UINavigationController which is required by the MoviesViewCoordinator instance. Ideally I would be using a dependency injection framework such as Swinject to pass the value around as AppCoordinator does not make any use of it, but rather it is pass through dependency. Copy and paste the following code to the AppCoordinator.swift file:
Finally to wrap things up, edit the AppDelegate to create and start the AppCoordinator after finishing launching the app through the application(_ application:, didFinishLaunchingWithOptions launchOptions:) function. Remember to keep a strong reference to the AppCoordinator instance in the AppDelegate. Your AppDelegate should look like the following:
All set. You can now run the app 🎉.
The app that we have create in this tutorial is not very complex. Applying Coordinators and Model-View-ViewModel is an overkill. As some apps grow in size they also grow in complexity. Patterns like Coordinators and MVVM are just amongst many that help reduce complexities and bring consistency to a project.
I hope you enjoyed this tutorial. Please feel free to like and share if you like it.
P.S. This is the first tutorial I write, drop me a comment and let me know what you think or if you would like to cover some topic.