Using @IBSegueAction with Tab Bar Controllers

Xcode 11 introduced @IBSegueAction so you can create a segue’s destination view controller in code with a custom initializer. This avoids the need to check segue identifier strings and having to make view controller properties that need initialization optional. See Better Storyboards with Xcode 11 for a recap. You create the segue action in the segue’s source view controller but how do you do that when the source is a tab bar controller or navigation controller?

The Setup

Here’s my storyboard with two view controllers embedded in a tab bar controller. I embedded one of the view controllers in a navigation controller:

Storyboard with root tab bar controller containing two view controllers

The name view controller has a single label that shows a name. We can avoid making the name property an optional by providing our own custom init(coder:) method:

// NameViewController.swift
import UIKit

final class NameViewController: UIViewController {
    @IBOutlet private var nameLabel: UILabel?

    private let name: String

    init?(coder: NSCoder, name: String) {
        self.name = name
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel?.text = name
    }
}

The other view controller is similar except that it shows an image view instead of a label. The problem is we have no obvious way to call our custom view controller init methods. You can try to control-drag from the relationship segue in the storyboard but there is nowhere to create the segue action:

Control-drag from the relationship segue in the storyboard

Your first thought might be to create a subclass of the tab bar controller and give it the @IBSegueAction methods:

// TabBarController.swift
import UIKit

final class TabBarController: UITabBarController {
  @IBSegueAction private func showName(_ coder: NSCoder) -> NameViewController? { ... }
  @IBSegueAction private func showSymbol(_ coder: NSCoder) -> SymbolViewController? { ... }
}

After modifying the storyboard to use our tab bar controller you can connect the relationship segue to the action in the controller:

Connecting segue to segue action in tab controller

Unfortunately, it doesn’t work. The segue action method is never called. The storyboard falls back to calling the default init(coder:) method which, since we didn’t implement it, causes a crash. The result is the same if you try with a navigation controller subclass.

Create A Parent View Controller

To get segue actions to work in this scenario embed the tab bar controller in a new parent view controller:

Tab bar controller embedded in a parent view controller

The parent view controller (RootViewController) can then take care of creating the two view controllers using our custom initializers. Note that I marked both methods with @IBSegueAction to make them visible to Interface Builder:

// RootViewController.swift
import UIKit

final class RootViewController: UIViewController {
  @IBSegueAction private func showName(_ coder: NSCoder) -> NameViewController? {
    NameViewController(coder: coder, name: "Tim Apple")
  }

  @IBSegueAction private func showSymbol(_ coder: NSCoder) -> SymbolViewController? {
    SymbolViewController(coder: coder, symbolName: "star.circle.fill")
  }
}

We can then connect the relatonship segue between the tab bar controller and the symbol view controller to the showSymbol method in the root view controller:

Connect relationship segue to showSymbol method in root view controller

Likewise, we connect the relationship segue between the navigation controller and the name view controller to the showName segue action method in the root view controller:

Connect relationship segue to showName method in root view controller

That’s all there is to it!

Apple doesn’t seem to have documented segue actions anywhere except the Xcode 11 release notes. We could use a technote for segue actions like this one for unwind segues: