Navigation in iOS apps
Published: 2021-05-03 22:13:37

*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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4d7ojjooyxzmxmakp789.png)
 
# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yia6dwzqmp3tl1z9ut2t.png)

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcpy4qpqmlbcip6lfcjk.png)

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0asufpp8z9oh7bksi8ez.png)

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.

```swift
class FirstScreenViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func navigateButtonPressed() { 
        let secondViewController = SecondViewController()
        present(secondViewController, animated: true, completion: nil)
    }
}
```

[Video](https://drive.google.com/file/d/1lL91lSqua-0LNz_e7dPCS7RbmzHj6SKU/view?usp=sharing)

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.

```swift
@IBAction func navigateButtonPressed() {
    let secondViewController = SecondViewController()
    secondViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen
    present(secondViewController, animated: true, completion: nil)
}
```

[Video](https://drive.google.com/file/d/1YQjW8BAcZUkD6EAuFJe02G8B6BCvUdz_/view?usp=sharing)

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.

```swift
@IBAction func goBackButtonScreen() {
    dismiss(animated: true, completion: nil)
}
```

[Video](https://drive.google.com/file/d/1ChJDxUY7h0xzGCm-Zf3SRW9aP5vU5qj7/view?usp=sharing)
 
# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/88slqe1b2ew6m7lhl1ax.png)

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

Remember this action: 

```swift
@IBAction func navigateButtonPressed() {
    let secondViewController = SecondViewController()
    secondViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen
    present(secondViewController, animated: true, completion: nil)
}
```

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

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

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fayx8ytraxevvxcdbjnf.png)
 
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:

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

        title = "Second"
    }

    @IBAction func goBackButtonPressed() {
        dismiss(animated: true, completion: nil)
    }
}
```

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81heud6k5g4oeuxc2cbj.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a8ae4m80a762vcrfndun.png)

```swift
@IBAction func thirdScreenButtonPressed() {
    let thirdViewController = ThirdScreenViewController()
    navigationController?.pushViewController(thirdViewController, animated: true)
}
``` 

[Video](https://drive.google.com/file/d/1w7CfXtXx17AxPp_Pnq1erR0RfCvp8E-8/view?usp=sharing)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j578zxxnk2kz3073gd3k.png)

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

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ojixtzl00ym8inv4711.png)

```swift
@IBAction func popButtonPressed() {
    navigationController?.popViewController(animated: true)
}
``` 

[Video](https://drive.google.com/file/d/1QfkGaCdhGdd_3k6eJmHfJLq7u0PzVfj2/view?usp=sharing)

# 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.