Hands-on 6.1 – Passing state between view controllers

In the previous module, we added two more screens to our app. We used a tab bar controller to switch between the top question and the profile screens. We also added a screen to edit the user’s profile.

At the moment, all our view controllers work in isolation.

This means that both the QuestionViewController and the ProfileViewController create their instance of the StateController. As I mentioned, this is not the correct approach. View controllers need to share state.

Another problem this isolation causes is that we cannot save the data entered into the EditProfileViewController.

In this module, we saw how view controllers communicate so that we can solve all these problems.

We will start by connecting the ProfileViewController and the EditProfileViewController to each other, allowing them to share state.

Singletons are not the correct way to share state among view controllers

Before we move forward, I have a warning.

You might find that some developers use a different design pattern to share state between classes, called the Singleton Pattern. You might even see this pattern in some iOS tutorials.

In short, a singleton is a class that can only have one single instance. When you try to create a new one, you get back the instance that already exists instead of a new one.

Singleton classes often have a class property to retrieve their unique instance. Here are some examples of singletons in iOS:

  • UIApplication.shared
  • UserDefaults.standard
  • NotificationCenter.default

Singletons give you the ability to access shared instance from anywhere in your code easily.

This can make sense sometimes, like in the examples above. For example, you never want to access any other instance of UIApplication other than the current one.

Many developers though use singletons to share state between view controllers. This gives the apparent benefit of not having view controllers communicate. And who does not like a quick shortcut.

I say that the benefit is apparent because, despite their simplicity, singletons have hidden but significant drawbacks.

These pitfalls are so relevant that many developers define the singleton pattern an anti-pattern. Anti-patterns makes your life more complicated instead of making it more comfortable. Singletons and, more in general, any globally accessed state, create many problems with concurrent code. Moreover,  make your classes hard to test.

I won’t go into the details of the singleton pattern in this course because you should almost never use it.

This makes, by definition, singletons a non-fundamental concept for iOS development. My advice is to avoid singletons at all cost and use other patterns instead. You can access the system singletons in rare, specific instances, but you should never create your own.

I have not used a singleton in years.

We will see the alternative in a moment. It will need a bit more code than using singletons, but it will be worth it. If you want to know more about the drawbacks of singletons, you can search on Google for “singleton pitfalls” or “singleton anti-pattern”.

Sharing state between view controllers through model controllers

What data should we pass to the EditProfileViewController?

Since the EditProfileViewController needs to edit the data of the user, we might think, at first, to pass it a User value. That would not be enough, though.

Recall that User is a struct, which is a value type. Passing it around creates copies, and changing one does not affect the others. We can’t update the state of the app through the User type alone.

We need classes to hold shared state because they are reference types. So, what we need to pass between view controllers are references to the model controllers that keep the state of our app.

At the moment, we have just one model controller in our app, the StateController. More complex apps might split their state across separate model controllers. For example, you could add one to hold cached network requests.

We need to pass a StateController instance from the ProfileViewController to the EditProfileViewController.

First of all, we need to make the EditProfileViewController able to receive a StateController instance:

Notice that this property needs to be optional. This is because the EditProfileViewController gets instantiated by a segue and we don’t have access to its initializer.

We can pass the state controller reference only later, but Swift requires all properties to be set at initialization time. The stateController property is optional, so it can get a nil value when the view controller is initialized.

Notice also that, while outlets are implicitly unwrapped (by the ! operator), this property is not.

What is the difference?

The idea is that the system usually sets outlets and never go away. It makes sense to declare them as implicitly unwrapped optionals, to make them more convenient to use. Moreover, Xcode shows you when they are not connected too.

We have to pass the stateController in our code explicitly. But we might forget, or a bug might cause it not to be set. To be sure not to crash the app, it’s better not to unwrap it implicitly. This will, of course, cause the appearance of unwrapping and optional binding everywhere we access the state controller.

The longer code is the price we pay for safety.

If you want to be really safe, you can even change the declaration of outlets. This can be especially useful when you manipulate the UI, and some views might go away.

Passing data forward when a segue is triggered

The EditProfileViewController comes on screen through a modal segue triggered by the Edit button in the ProfileViewController.

When the segue gets executed:

  1. A new instance of EditProfileViewController is created.
  2. The prepare(for:sender:) method of ProfileViewController gets called.

The latter is where we can pass data to the EditProfileViewController before it becomes visible on the screen.

In this method, we can get a direct reference to the new EditProfileViewController instance through the segue parameter. This carries a reference to the new view controller in its destination property.

This method is executed right after the initialization of the EditProfileViewController, but before its lifecycle goes on.

When viewDidLoad() is called, the stateController property is already set, so we can use it to populate the user interface.

When we run the app, the Edit Profile screen is filled with the correct data.

Passing data back through the shared state of the app

To save the user edits, we have to fill the save(_:) method in EditProfileViewController.

Here, we need to update the user property in the shared StateController instance. Since the two view controllers now share this instance, any change will be available to both.

I also added some validation code to check for empty values. This is just an example and is not sophisticated. A real app would need to check the two values separately and give better messages.

My point is just to show you where you should place such code. You always need to validate data before committing it to the state of the app.

The changes are now available to the ProfileViewController view controller too. But if we go back, we see that the previous screen still holds the old data.

 

A view controller does not update its interface automatically. We need to trigger this explicitly.

We already have code that populated the ProfileViewController interface in its viewDidLoad() method. This gets called only once, at the beginning of a view controller’s lifecycle, so it won’t be called when we go back.

Recall from Module 3 that if you want to execute some code every time a view controller comes on the screen, you need to use the viewWillAppear(_:) method. That’s how you update the interface of a view controller when new data is available.

Now the user profile screen shows the correct, updated data.

Key takeaways

  • Singletons are not the right way to pass state between view controllers. They can be useful in some cases, and some classes in iOS frameworks use them, but you should not create your own. There is a price to pay for the simplicity of singletons. Any globally state can generate concurrency problems, and it makes objects hard to test.
  • State is shared between view controllers using model controllers. By design, our model is made of value types, while to share state we need reference types.
  • You can pass data to back to other view controllers by updating the shared state. This is not enough to update other view controllers. To update the interface of a view controller before it becomes visible again, you use its viewWillAppear(_:) method.

Lesson tags: ios-professional
Back to: The Confident iOS Professional (UIKit Version) > Module 6 - How view controllers communicate: Triggering Transitions and Preparing for Segues