Hands-on 6.2 – Building a complex navigation structure with navigation controllers

At the moment, we can move between a few screens in our app:

  • We can switch between the Top Questions and the Profile screen (the former still contains only one question. We will fix this later).
  • We can go from the Profile screen to the Edit Profile screen and back.

The profile of the app’s user is not the only profile we want to display. Every user on the Lifehacks website has a profile, and we want to show those too. The profile of any user looks precisely the same, so we want to reuse the ProfileViewController.

We will now see how a view controller in a storyboard can be reused for different purposes and in different navigation paths.

Using simple controls to detect user interaction instead of complicated solutions

Currently, our ProfileViewController is a child of the tab bar controller. Now we want to go to the full profile of the owner of a question.

We already display the owner on the Question screen. All we want is to make it interactive. Here is where I often see many developers complicating their life.

There are many ways to detect taps in iOS. You can use a gesture recognizer or the multitouch API of UIView.

But there is an easier way.

We can place a simple button over the views we want to make interactive. This requires minimal changes in our UI and is simpler to use than a gesture recognizer. I prefer to use the latter when I have more complicated interaction patterns in my app.

The image view and the labels for the owner of a question are already inside of a stack view. All we have to do is add a button over it, with the same position and size.

This stack view changes size according to its content, so we need to add constraints that will keep the button in sync.

The easiest way is to use the control–drag method we saw in past modules:

From the pop-up menu, select the Top, Leading, Equal Widths and Equal Heights constraints. You can select all of them at once by holding the cmd key.

Remember to remove the “Button” text from the button to make it invisible. Even if you can’t see it, it still works as expected and tapping on it will trigger any connected action.

No need for complicated solutions.

Combining different containers to achieve complex navigation

If we connect the new button to the ProfileViewController now, we will get a modal segue. This is not the place for modal navigation, though. Modal presentation interrupts the flow of the app to ask the user for information or actions.

As you should know by now, for any navigation that is not modal presentation, we need to use a container. Since we are drilling into the details of an item, we need a navigation controller.

Containers are also view controllers. This means that we can put them in other containers. This is important because the combination of different containers is what allows you to create complex navigation structures in your apps.

Currently, the QuestionViewController is the first child of the tab bar controller. To go from it to the ProfileViewController, we need place it inside a navigation controller instead. This navigation controller, in turn, goes inside the tab bar controller.

Navigation controllers are not like tab bar controllers. Even if we will navigate from the QuestionViewController to the ProfileViewController, only the first needs to be contained.

This view controller is called the root view controller. Like other contained controllers, we connect it to the navigation view controller using a relationship segue. Since this is not an action segue, recall in mind that it does not trigger any transition and the prepare(for:sender:) method is not called. It’s just a placeholder.

So, practically speaking, we need to connect the tab bar controller to a navigation controller through a relationship segue, and then connect the navigation controller to the QuestionViewController using another relationship segue.

You could connect them using the usual control-drag mechanism in Interface Builder, but utilizing the Editor -> Embed In -> Navigation Controller menu is faster since it puts all the connections in place for you:

The view controllers contained in a navigation controller get a navigation bar at the top. When you set the Auto Layout constraints to the margins of the view, as we did, all your views will move down automatically to make space for the bar.

You can select a navigation bar and change its properties in the Attributes inspector. I gave this one the Question title.

Navigation bars do not belong to view controllers

We can now connect a segue from the QuestionViewController to the ProfileViewController.

As for other segues, control-drag from the button to the destination view controller and select the “show” action segue in the pop-up menu. This segue triggers a transition in the navigation controller to bring the ProfileViewController on screen.

There is one thing I want to draw your attention to since this confuses some developers.

Notice in the above picture that the ProfileViewController also got a navigation bar at the top, with a back button to go back to the previous screen. This is because this view controller is now reachable through the drill-down path of a navigation controller.

But it’s also contained in the tab bar controller. The navigation bar, in that case, makes no sense. There is no previous screen to go to.

In fact, if you run the app, you will see that the ProfileViewController gets a navigation bar only when you reach it tapping on the owner of a question. If you go to the Profile tab in the tab bar controller, there is no navigation bar at the top.


The reason is that the navigation bars you see in a storyboard are not real navigation bars. They are navigation items associated to a view controller. These allow you to set some properties related to a specific view controller, like the title and buttons.

View controllers do not have a navigation bar of their own, only navigation controllers do.

The navigation bar you see when you run the app belongs to the navigation controller. The transition from one view controller brings in the navigation item for the corresponding view controller, but the navigation bar stays the same.

That is why the ProfileViewController gets a navigation bar only when you reach it from the QuestionViewController. That navigation bar belongs to the navigation controller.

If we look at our mockup, we want the ProfileViewController also to have a navigation bar.

This is not just my arbitrary choice. It’s very common for iOS apps to have a navigation bar, even if there is no navigation. You often want to set a title, buttons or a search bar at the top of a view controller.

Providing navigation bars by adding navigation controllers

Now, the temptation would be to reach for the Object library and drag a new navigation bar on the ProfileViewController.

Don’t do that.

Navigation bars don’t behave appropriately outside of navigation controllers. You can’t extend them to the status bar, and you can’t easily replicate hiding on scrolling, big titles or resize them when you rotate the device.

The solution, instead, is to embed every view controller that needs a navigation bar into a navigation controller in any path that leads to them. Every time a navigation controller contains a view controller on one path, any other path that leads to the latter should include a navigation controller too. There are rare exceptions, but this is a generally safe rule.

So we need to add a second navigation controller in the tab bar controller, for the Profile tab, even if there is no drill-down navigation going from it.

We also added a navigation controller for the Edit Profile screen, which also needs a navigation bar.

But why?

The only path that leads to the EditProfileViewController already contains a navigation controller. Shouldn’t this be enough?

It isn’t because the path is interrupted by a modal segue. Even though the ProfileViewController is the one that presents the EditProfileViewController, the presentation happens on top of every other view controller, including any container.

The existing navigation controller gets covered by the presented view controller, so you can’t rely on its navigation bar. You need a new one, and thus, a new navigation controller.

Beware that this structural change also changes how the state is propagated from the ProfileViewController to the EditProfileViewController. The presented view controller is now the navigation controller, and we need to consider this in the prepare(for:sender:) method of ProfileViewController.

The UINavigationController class has a viewControllers property that contains all its children. This is also true for other containers.

In there, we can find the root view controller instance to which we can pass the state controller:

As a final note, I also replaced all the temporary buttons I placed in the ProfileViewController and EditProfileViewController with buttons in the navigation items. These come already with predefined system values. You don’t have to decide which text to put into them, which makes it easier to follow Apple’s Human Interface Guidelines.

In our mockup, the navigation bar has a tint color, but we cannot set that in the storyboard. It requires some code that I will discuss in the next module.

Key takeaways

  • Many developers tend to use complicated solutions where simpler ones suffice. One of these is responding to touch events. Most of the time there is no need for gesture recognizers or the multitouch API. Buttons in iOS are highly customizable and cover many use cases.
  • Containers are view controllers themselves. This means that you can put containers in other containers to achieve more complicated navigation flows in your app. A typical combination is to use a navigation controller for every tab of a tab bar controller.
  • Navigation bars belong to navigation controllers, not to view controllers, as some developers believe. This means that when you want a navigation bar at the top of your interface, you need a full navigation controller, even if there is no navigation going forward.





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