iOS Bug of the Week: View Controller Gets Pushed Multiple Times
Tags: ios, swift, mobile
Published: 2022-07-13 02:28:16

## Introduction
In iOS Bug of the Week (not necessarily weekly), I'll go over an interesting edge case bug that hasn't been heavily documented due to its uncommon or specific nature.

For this installment, I'll discuss an interesting navigation issue that showed up when pushing a detail view controller to a `UINavigationController`'s stack.

## The Setup
I aimed to create a reusable button style with default values so the app can have a consistent look and feel for all of our buttons.

To achieve this, I extended `UIButton` with a set of properties I would like to apply to any button of this type.
```swift
extension UIButton {
    static let defaultStyle: UIButton = {
        let button = UIButton()
        button.layer.cornerRadius = 15.0
        button.backgroundColor = .blue
        return button
    }()
}
```
With this extension, the caller can easily create a button that keeps a style consistent with other buttons using this configuration.

So far so good. Let’s put this button into use.

#### Overview of our example
To keep this example simple, we will use our button in a parent view controller to navigate into a detail view controller by pushing it to the navigation stack.

In our example, we can also “log out” from the parent view controller onto a login screen, which will be separate from the parent-to-detail navigation flow.

#### Creating our screens
In our parent view controller, `MainViewController`, let's create a `lazy` instance of `UIButton`, which will use our button extension. Therefore, it should inherit all the properties from `defaultStyle`.
```swift
class MainViewController {
    private lazy var proceedButton: UIButton = {
        let button = UIButton.defaultStyle
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
}
```
Now let’s assign a selector to this button in the `viewDidLoad` of our view controller.

With our selector, we'll create an empty `DetailViewController` and push it to the navigation stack.
```swift
@objc
private func didTapProceedButton() {
    let viewController = DetailViewController()
    navigationController?.pushViewController(viewController, animated: true)
}

override func viewDidLoad() {
    super.viewDidLoad()
    proceedButton.addTarget(self, action: #selector(didTapProceedButton), for: .touchUpInside)
}
```

## The Bug
Loading up the app, we click on the proceed button, and it navigates to the detail view controller. No issues there.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/db13pey7dqx675i6jqbb.gif) 

But what would happen if we navigated to another part of the app and re-navigated back to `MainViewController`?

For this example, say I decided to click on some logout button and log back into my `MainViewController`.

When I log back into the `MainViewController`, clicking the proceed button will now push the detail view controller to the navigation stack twice.

Repeating this process again will cause the button to now push three instances of the detail view controller. And so on and so forth.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h067v9liv2kd6xm38650.gif) 

We observe that this behavior is consistent with the number of times we navigate back to `MainViewController`, but what is causing this bug?

## The Fix
The issue ended up being caused by the setup of the `UIButton` extension.

Notice in the code for our button style that we're calling this function with the `()` syntax and then assigning its result to `defaultStyle` as a `static let` property.
```swift
// Incorrect approach
static let defaultStyle: UIButton = {
    let button = UIButton()
    button.layer.cornerRadius = 15.0
    button.backgroundColor = .blue
    return button
}()
```
Why is this problematic? Well, it's only created once, so anytime we call this button extension, we'll be getting back the same instance wherever else it's used.

Let's make a fix to this setup:
```swift
extension UIButton {
    // Correct approach
    static func defaultStyle() -> UIButton {
        let button = UIButton()
        button.layer.cornerRadius = 15.0
        button.backgroundColor = .blue
        return button
    }
}
```
In our new approach, we are deferring the creation of our button to whenever that function is called. This creates a new `UIButton` instance every time, as opposed to calling the same instance.

Recall also that we assigned the selector to the button in the `viewDidLoad` of `MainViewController`. Therefore, every time we load this view controller, we would be stacking that selector onto the button yet again.

Since it's the same instance of the button, it will retain each iteration of the selector. So every time the user taps the proceed button, it will trigger every instance of the selector that has been assigned upon loading the view controller. Hence the reason multiple instances of `DetailViewController` get pushed for one button press.

As you can see, the bug is created through multiple steps, and it's important to identify where the root cause is.