The Ultimate Course to Making Professional iOS Apps – Lesson 5

The Ultimate Course to Making Professional iOS Apps

Lesson 5: making view controllers communicate and sharing state across the whole app

In the past four lessons we explored all the layers of the MVC pattern. We started with the model, representing the data, its transformations and the business logic. We saw how model controllers deal with state and storage, among other things. Then we moved to the view controllers and how they define the screens and the flow of the app. Finally, we saw the view layer, with Auto Layout and how to transfer data and respond to user action.

We now need to glue all these pieces together to have a working app. Again, as I said at the beginning, this is not the order in which you would probably make a new app. Still, it shows you that there are different ways you can approach the task. I made this mini course to give you a full panoramic of the high level picture of iOS development.

Transferring state through dependency injection

We saw how model controllers keep the state of the app and manage the model, while the view controllers deal with the flow of the app.

It is clear that all the view controllers in our app need to get access to the central state of the app. This means that they need to access the model controllers responsible for it.

But this is not enough. If all view controllers just had their own copy of model controllers, they would all access different copies of the app state. When one view controller changes its state, others would not see this change. View controllers clearly need to access a central state which is shared among all.

Given their nature in the flow of an app, it is also true that view controllers do not act as silos, only accessing the app state. What a view controller displays often depends on what happens in the previous view controller. If the selects of some element in the previous screen, the new view controller coming on the screen will need to show the details of that specific element.

So we can come to these conclusions:

  • view controllers need to access the same instances of the model controllers
  • view controllers need to communicate with each other

Unfortunately many developers seem not to understand how iOS works nor they follow good software development principles. As a result they address these two with solutions that create a lot of problems. Among these, notable ones are:

  • singletons
  • using the app’s delegate

In iOS, each view controllers goes through a specific lifecycle, with events that happen in a specific order. There is a specific part of this lifecycle that allows view controllers to communicate with each other. Whenever a segue is triggered, the prepare(for:sender:) method is called in the origin view controller. Through this method, you gain access to the destination view controller and can pass data from one to the other.

It is clear that this solves our communication problem, but it is less clear how this also solves the problem of sharing state across the whole app.

We do this through a concept called dependency injection. Dependency injection states that an object should not create or retrieve the objects it needs to function, but it should receive them from the outside.

So, to share state across an app, dependency injection means this: a view controller does not create instances of model controllers. Nor looks for them through mechanisms like singletons or the app delegate. A view controller needs to get such model controllers from some other objects. And these other objects are (in general) other view controllers.

In the moment two view controllers will be connected through a segue, the origin will not only pass some  information to the destination. It will also inject all the instances of model controllers into the next view controller.

Creating shared instances of model controllers

Let’s see how we can propagate state in our app. We have two model controllers, the StateController and the StorageController.  As we have seen, the second is already managed by the first one. Our view controllers only need an instance of the former. Since they need to receive it from the outside, they all need a property to allow the passing of this shared instance:

No view controller creates this instance, but we still have to create it in some place. We do this inside of AppDelegate, when the app starts. From there we pass it to the first view controller of our app, which is the AccountsViewController. We have to keep in mind that this view controller is contained in a navigation controller.

In this method I also set the global appearance of the app, which in this case is only about the colors of navigation bars and their titles and buttons. I created a little struct to keep this functionality separated:

Data sources for table views are model controllers too

We now need to populate the UI of our first view controller with data. The AccountsViewController shows a list of accounts through a table view. Table views receive their data through an external object called data source. This data source needs to implement a couple of methods that the table view will call to ask for the data it needs.

This data source could be the view view controller containing the table view. In fact, many developers do this, but this is not a good solution. It is better to create a separate model controller for this task.

All this class does is:

  • get a list of accounts
  • return to the table view the number of rows
  • return to the table view already populated cells

The table view asks for this information when it needs it.

Notice how this data source uses the view models we created in the previous lesson to pass the account data to a cell. So it does not need to deal with converting data.

Now that we have a data source to populate the table view, all the view controller needs to do is

  • get such data from the StateController
  • give it to the AccountsDataSource
  • connect the data source to the table view.

We do this inside of the viewWillAppear(_:) method of the view controller, so this code is executed every time the view controller comes on the screen. This means that after we add a new account in the CreateAccountViewController and come back to this screen, the data in the view controller will be updated through this method.

How view controllers communicate

If we run our app, we still won’t see any account in the first screen. There is still no data in it. We need to allow the user to actually create new accounts through the CreateAccountViewController. Then, when the user selects an account in the list, we need to pass this account to the TransactionsViewController to show its details.

Remember that we already put in place all the infrastructure to navigate to both these view controllers. We connected segues to both the add button and the cells of the table view in the lesson on view controllers. All we have left to do is pass data between view controllers when these segues are triggered.

As I said above, this happens inside the prepare(for:sender:) method. Since there are two segues originating from the AccountsViewController (one to the CreateAccountViewController and one to the TransactionsViewController) we have to check which one of the two is actually happening.

The distinction between segues is done through their identifier, which is a string you can set in the storyboard. You can see how to do it here.

In the case of the CreateAccountViewController we pass the StateController instance only. To the TransactionsViewController we also need to pass the account that was selected by the user.

Now that the CreateAccountViewController has an instance of StateController, it can actually use it to create a new account when the user taps on the save button.

Since an unwind segue is triggered when the user taps on the cancel or save button, the prepare(for:sender:) method is called in the CreateAccountViewController for both of them. We create the new account only in the case that the segue is the one for the save button.

Backwards communication through delegation

The TransactionViewController works pretty much in the same way as the AccountsViewController.

First of all, we need a data source for its table view too:

Then in the view controller’s code we do the same we did for the account view controller: we create the data source with the transactions of the account and connect it to the table view. We also populate the two labels in the header of the table view with the data of the account:

Here we have a small problem. A transaction is not independent from its account. This means that a new transaction cannot be added alone to the StateController. We need an account. We could of course pass the account to the CreateTransactionViewController from the previous view controller. But since the CreateTransactionViewController would not need this account for anything else, I will take the chance to show you a different way to establish view controller communication: delegation.

Instead of receiving an account and the StateViewController, the CreateTransactionViewController can define a delegate protocol. Though this protocol it will communicate the creation of a new transaction to another object:

This protocol does not say which object needs to be the delegate. Only that it needs a method to receive a new transaction.

As you can see in the prepare(for:sender:) method, the view controller does not save the new transaction, but passes it to its delegate. This delegate will be the TransactionsViewController itself.

To set this link, the TransactionsViewController conforms to the CreateTransactionViewControllerDelegate protocol. It then sets itself as the delegate of the CreateTransactionViewController in its own prepare(for:sender:) method, instead of passing forward an instance of the StateController:

The implementation of the add(newTransaction:) method adds the new transaction to the account and then updates it in the StateController.

Our app is complete. You can run it and create new accounts and transactions. If you close and relaunch the app you will also see that the data is saved and not lost.

Summary

This was the last lesson of this mini course on building professional iOS apps.

In the lessons of this mini course we are moving fast and glossing over details. There is no time or space here to go over them, since what I want to give you here is an overview and understanding of how professional apps are structured. I will share with you more free material on the various aspects of iOS development after this mini course ends.

What is important to stress here is that:

  • the state of an app is contained in unique instances of model controllers, which are created in a central place and not by view controllers
  • these shared instances are passed among view controllers through dependency injection when segues are triggered
  • view controllers should not act as the data source for table views. It is better to create specialized model controllers for this purpose
  • backwards view controller communication can happen not only through unwind segues. Delegation is a valid and sometimes better alternative

If you want to go more deeply into the all the details, my “The Confident iOS Professional” Course does so. It took me two years to complete it and to make sure I included every important aspect of developing complete iOS apps. The course opens regularly only to subscribers of my list. So if you are interested stay in my list and you will be notified when it opens.

Here is a preview of the modules in the course related to what we saw in this lesson:

Module 5 – How to Structure the Flow of any App: View Controller Presentation and Container View Controllers

  • How the flow of different screens is actually created in iOS. This is what enables you to use the standard transitions of iOS without having to re-invent them yourself
  • How view controllers present other view controllers on the screen. This is the common interaction pattern to use when you need to interrupt the app flow to prompt for an important action from the user
  • How to create apps with many screens and complex flow using container view controllers. This is what makes the difference between a sample toy app and a real world app
  • The available container view controllers of iOS which allow you to structure the flow of an app with many screens and different interactions

Module 6 – How View Controllers Communicate: Triggering Transitions and Preparing for Segues

  • Which code is executed when a segue is triggered and in which exact moment you can pass data to the next view controller. Many developers miss this and use wrong patterns instead (singletons or the app delegate)
  • How you can still trigger transitions and pass data between view controllers even when you are not using segues

Module 7 – The Delegate Pattern: Handing off Responsibility to Other Objects and Notifying Events

  • The next fundamental design pattern in iOS development, used by many core classes of iOS
  • The correct way to pass data backwards between view controllers. Many beginners miss this and rely on other problematic approaches that create problems they are not even aware of

Module 8 – Displaying Lists and Hierarchies: Table Views and Data Sources

  • How table views are the correct way to display long lists of elements and why other approaches lead to slow user interfaces and memory problems
  • How table views interact with their delegate and data source when they are populated. Many developers are surprised when they fist learn about this model of interaction
  • How to write delegates and data sources in a more reusable and manageable way. This is a common mistake that leads to bloat view controllers with too much code that should go into other classes

Master Module 3: Distributing and Deferring Calls – Notifications, Notification Centers and Timers

  • How the broadcast model of iOS apps works exactly. This is an indispensable model you need to understand to take advantage of app wide notifications in the correct way.
  • The hidden complexity notifications introduce in your app and the problems they might cause. Online tutorials never talk about this important aspect of notifications.
  • How to limit the scope of your custom notifications so that you can limit unexpected problems. Failing to do leads to bugs that are hard to find and understand.
  • How to deal with notifications shared among many different objects. You need to understand the implications of this to coordinate many receivers correctly.
  • The implications of scheduling delayed or repeated calls through timers. Miss these and you might get delayed timers or callbacks on unexpected threads.

Master Module 4: Advanced Architecture for Complex iOS Apps – Injecting Dependencies and Managing Flow with Custom Storyboards and Segues

  • The key points of the MVC pattern that create serious architectural problems in complex apps. If you don’t know how to identify these points, you will end up with a garbled code base that is hard to disentangle.
  • How to take the important duty of dependency injection out of view controllers, where it does not belong. Not taking care of this leads to a lot of unused and confusing dependencies in all your view controllers.
  • How conditional app flow in view controllers creates unnecessary and problematic coupling. When you do this you find yourself with complex code to do something that could be done in an easier and cleaner way elsewhere.