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:
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:
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:
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:
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:
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:
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: