DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on • Updated on

Navigation in iOS apps

Note: This article is part of the course Introduction to iOS using UIKit I've given many times in the past. The course was originally in Spanish, and I decided to release it in English so more people can read it and hopefully it will help them.

Introduction

So far, we've seen example applications where we only had a single view in the screen. In real world apps we have several screens per app. The user starts their journey in one of them but depending on the actions they take, the app will show them other screens.
The ability of moving between screens in an app is called navigation.

Stacks

What is a stack?

  • A stack is a data structure in which we can insert elements.
  • Whenever we add an element to the stack (push), it will be put at the top.
  • If we remove an element from the stack (pop), we'll always remove the top element in the stack, so the element that was below the top, will become the top after that.

stack-01

Navigation Stack

In iOS, navigation works using one or many stacks. Each element in the navigation stack is a screen, and each screen is represented by a UIViewController.
The element (screen) that is in the top of the navigation stack is the screen that's visible to the user in the app.

Present/Dismiss

There is a "global" navigation stack in iOS. We don't have to do anything to create it. Just by creating an app in iOS, we'll have that global navigation stack ready to be used.
Every UIViewController subclass have a present and a dismiss methods.

Let's see an example:

image

image

image

If we want to navigate from FirstScreenViewController to SecondScreenViewController, we need to:

  • Instantiate SecondScreenViewController from a method on FirstScreenViewController.
  • Call the present method right after that, passing to it the SecondScreenViewController instance, a Bool indicating whether we want to present the screen using an animation transition, and optionally, also a closure that will be executed as soon as the transition has finished.
class FirstScreenViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func navigateButtonPressed() { 
        let secondViewController = SecondViewController()
        present(secondViewController, animated: true, completion: nil)
    }
}
Enter fullscreen mode Exit fullscreen mode

Video

Optionally, we can configure the property modalPresentationStyle in SecondScreenViewController with the value UIModelPresentationStyle.fullScreen before navigating to it, so the destination view controller will fit the full size of the screen.

@IBAction func navigateButtonPressed() {
    let secondViewController = SecondViewController()
    secondViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen
    present(secondViewController, animated: true, completion: nil)
}
Enter fullscreen mode Exit fullscreen mode

Video

The problem we have now is that, as it's now fullscreen, we can't go back to the first screen anymore. To avoid this, we can configure an action in the second screen, using the dismiss method.

The dismiss method takes two parameters: a Bool indicating if we want an animated transition, and an optional closure, in case we want to execute custom code after the transition has finished.

@IBAction func goBackButtonScreen() {
    dismiss(animated: true, completion: nil)
}
Enter fullscreen mode Exit fullscreen mode

Video

UINavigationController

However, the practice of doing present/dismiss is commonly used to show a modal to the user in specific situatioons.

Most commonly, we'll use UINavigationController, which is a class that contains an internal navigation stack.

To add a screen/controller to a UINavigationController, we'll use the push method.

To remove a screen/controller from a UINavigationController, we'll use the pop method.

A UINavigationController is instantiated with a UIViewController that's called the root view controller, and that will be the bottom of the stack.

In addition, using a UINavigationController gives us a view at the top of our screens that's called UINavigationBar, that contains the current screen title, helper buttons at the sides, and back button, among others.

image

Let's try to modify the second screen of our app, so that it will be contained inside a UINavigationController.

Remember this action:

@IBAction func navigateButtonPressed() {
    let secondViewController = SecondViewController()
    secondViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen
    present(secondViewController, animated: true, completion: nil)
}
Enter fullscreen mode Exit fullscreen mode

We'll modify it so that secondViewController will be inside a UINavigationController.

@IBAction func navigateButtonPressed() {
    let secondViewController = UINavigationController(rootViewController: SecondViewController())
    secondViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen
    present(secondViewController, animated: true, completion: nil)
}
Enter fullscreen mode Exit fullscreen mode

image

The view that we see at the top is the UINavigationBar.

Something we can do to personalize it, is to assign a title to it. To do so, we'll modify the property title in the view controller:

class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Second"
    }

    @IBAction func goBackButtonPressed() {
        dismiss(animated: true, completion: nil)
    }
}
Enter fullscreen mode Exit fullscreen mode

image

In order to perform a transition in a UINavigationController we need to:

  • Create another screen. I mean, a new UIViewController
  • Inside SecondScreenViewController, perform a push to the third screen.
  • To do this push, let's keep in mind that the class UIViewController has an optional property called navigationController with type UINavigationController?.

image

@IBAction func thirdScreenButtonPressed() {
    let thirdViewController = ThirdScreenViewController()
    navigationController?.pushViewController(thirdViewController, animated: true)
}
Enter fullscreen mode Exit fullscreen mode

Video

You might have noticed that the navigation bar automatically adds a button to return to the previous screen. If we want to do a back navigation programmatically, we'll use the pop method from UINavigationController.

image

Small comment: to add a padding to the button, we can set the content insets of it in the attributes inspector

image

@IBAction func popButtonPressed() {
    navigationController?.popViewController(animated: true)
}
Enter fullscreen mode Exit fullscreen mode

Video

Passing data

How could we send data from one screen to the next one. The answer is, that before performing the navigation, we can set a variable in the destination UIViewController.
We must NEVER configure a view on the destination controller, because they aren't initialized until the navigation is committed.

Discussion (0)