Effective iOS App Architecture

Most iOS projects are in a bad way when it comes to software design.  I have seen (and had to work on) 5000 line view controllers.  I have seen business logic of a main view controller be split out into categories on that view controller.  I'm not the only one who has opened an AppDelegate.m and had my eyes begin to water. 

I've been using a pattern for a while now which I have found great with both new projects and nightmare legacy codebases.   It is quite similar to VIPER but a bit easier to introduce to existing projects.  It's also easier to grasp just by looking at a project's files; code should be bus-proof (the developers of a project get hit by a bus, and that project gets dumped on you), and I feel that VIPER is overly complicated with bizarre terminology (what are all classes if not 'interactors'?).

If you'd rather see a concrete example of this pattern than read a description of it, then take a look at this barebones example app.

Problems in typical iOS apps to overcome

  1. UIViewControllers are huge, because:

  2. UIViewControllers contain business logic.  Business logic being code which defines what to show the user - like a formatted string based on the price property of some object, or code to determine whether to show a postage label based on multiple properties of a product object.

  3. UIViewControllers contain the delegate and datasource methods for a UITableView or UICollectionView.  Those methods consist of sprawling conditionals determining what to show for a given index path.

A Solution

  1. Each UIViewController has a corresponding view model.

  2. View controllers have an array of section managers, with one section manager for each section of the table/collection view.

  3. View models talk to data providers, which in turn talk to Coredata for persistence, and API clients for getting data from the network.

Here's what the setup should look like:

Role of each class:

API Client:

Calls an API (or logical group of APIs) and parses the response into digestible NSDictionary objects.  This is where you might want to clean up or standardise some crappy API response data for use in your data providers.

Data Provider:

Will fetch data from API client objects and parse them into actual objects and, optionally, deal with persistence (such as Coredata).  A data provider may call on multiple API clients to provide data for some logic portion of your app's functionality.

View Model:  

Contains code which processes and calculates what needs to be shown in your view controller.   It does not contain code which process how to show that data.    View models may be reused between view controllers.

Your object model will be referenced in the view model, and not in your view controller.  Say you have a Product class that represents your company's product.  Instead of having an array of products in your view controller,  you keep that array internally in your view model.  You then have methods on your view model which return data from those products.  Let's say you have a label on your view which will show the price of a product, with or without tax depending on the type of the product.  You might do all this in your view controller.  But this is business logic, which you might reuse somewhere else.  So put it in your view model.   Then have a method on your view model - like priceTextForItemAtIndex That method will do the heavy lifting of determining what text to show for a given product.

If the data your are showing in your view controller is huge (you have 15 sections, with tons of data being presented) you can have sub view models referenced as properties on your view model.  Have a whole section for a product's shop information?  Make another view model just for shop data, and reference it as a property of your main view model.  This also make reuse easier.

View Controller:

Your view controller will be relatively minimal and will serve as the responder to section manager delegate methods, and be the hub for navigation.  It will have an array of Section Managers - one for each section of your table/collection view.

It will conform to UICollectionViewDatasource and UICollectionViewDelegate (or UITableView equivalent).  But instead of implementing those methods and populating cells, calculating cell heights etc., it will pass that functionality through to the appropriate section manager.

The entirety of your cellForItemAtIndexPath will look something like this:

if let managers = self.sectionManagers {

        let sectionManager = managers[indexPath.section]
        return sectionManager.cell(forRow: indexPath.row)

}

Section Manager:

Section managers will be objects which implement methods for:

  1. Returning number of rows.
  2. Returning height for a given row.
  3. Returning a UICollection/TableViewCell (or subclass).  
  4. Responding to tapped cells or other actions inside cells.

A view controller and its section manager will share the same view model.  

There are two options to handle navigation.  One is to have the view controller pass didSelectItemAtIndex through to the section managers, and each section manager will interpret what that tap means, and fire a delegate method on the view controller such as

-(void)sectionManager:(SectionManager *)sectionManager didSelectDoSomthingWithItemAtIndex:(NSInteger)index

and then the view controller would handle the navigation associated with that action.

The other is to have the view controller just respond directly to didSelectItemAtIndex rather than passing it through to section manager.  

The first option is a little more convoluted but it keeps the interpretation of an action in the logical place - the section manager.

Implementing in older projects.

Take it one view controller at a time.  This pattern doesn't need to take over your entire project at once. 

Rather than trying to refactor your existing view controllers in site, create a new one along side your old one.  

  1. If your API fetching/processing takes place in your view controller (you poor soul) then first thing is to write any API client(s) for the APIs you need to hit.  Then create any data providers required for your view controller.
  2. Create your view controller, view model(s), section managers for replacing an existing view controller.
  3. Go through your old view controller with a fine-tooth comb and scrape out any business logic and re-implement it in your view model as logically as possible.  Remember to keep your object model(s) inside your view model and have a method or property for sharing that object's data to the outside.
  4. Take this opportunity to redo your VC's UI with Autolayout if it isn't already.  Flesh out your section managers one section at a time until you have recreated your UI.

Little by little you'll replace all that crappy code with something that is a pleasure to work on.  An advantage of having everything logically separated is that once you set up your scaffolding, other team members can drop in when they have time and contribute easily. 

Why bother

There are a whole bunch of advantages to using this approach. Yes, you will end up with lots of files. But so what?  When you are hunting a crasher in a legacy code base, and you're scrolling through a 5000 line view controller you'll wish that functionality was chopped up in to some semblance of logical pieces, no matter how numerous.

Here are the main advantages I've found with this approach:

  1. View controllers are smaller, and are basically just hubs for responding to delegate methods from the view model and section mangers, and handling navigation.
  2. It's very easy to find the code you're looking for by navigating to the appropriate section manager.
  3. Table/collection view datasource/delegate methods on your view controllers are not massive conditional nightmares.  This is good for your emotional wellbeing.
  4. You can reorder your sections simply by changing the order of your section managers in the array on your view controller.
  5. Adding new features to your view controller no longer means adding complexity.  Your view controller is not going to be any more complex by adding another section.  You will need to add code to your view model, but as mentioned before, you can separate your view model into logical pieces with one main view model and several sub view models as properties on the main one. 
  6. View models can be reused.  
  7. View models, by their nature, lend themselves to be unit testing without you having to keep unit tests in mind while you implement features.
  8. If you have a list-detail type app with edit/save functionality, you can have your model contain your editing state.  Have properties (declared internally in your .m) representing your object's properties.  Those get edited and changed in your UI.  When you save, you copy them to your object and save its context.  If you cancel, you do nothing.

An actual real-world example

I have created a bare-bones app with a couple of view controllers for you to play around with - try it out and see what you think.  You may have ideas on how to improve this pattern please feel free to share it.