The other day I had what I thought a very simple request: I wanted to replace one viewcontroller with another in the same window. This is something that on iOS (particularly iPhones) happens all the time; but somewhat to my suprise, macOS does not have an obvious out-of-the-box solution for this.
This turns out to be a much more interesting question than it seemed at first because it quickly arrives at architecture and best practice: the question is less how to do this (there are several ways), but _whether_.
The NSStoryboard documentation says ‘in macOS, containment (rather than transition) is the more common notion for storyboards‘ which gives an intrigueing hint; we will explore this further once we have dealt with the common and/or obvious segues.
Since we’re here, I thought I’d write up all of the storyboard segues so I don’t have to do this again any time soon.
All instructions are for Xcode 9b3, Swift 4, OS 10.12.
standard segues
0) Create a new macOS application. Use Storyboards.
1a) Add a label (‘Initial View Controller’ and a button (‘Show Segue’) to the initial view controller. Name the window ‘Initial Window’
1b) Create a second view controller. Give it a label (‘Show ViewController’) and a button (‘Sheet Segue’).
1c) Drag from the ‘show segue’ button to the new view controller; choose ‘show’ from the menu.

(the ‘segue’ coming in is the ‘content view controller’ relationship automatically set up by Xcode).
1d) Build and Run.

You notice that the second view controller is shown in a new window, but since we haven’t specified a window controller for it, it is merely generic and has no title. You will also notice that while the app saves the location of the original window, this one will open up at the coordinates specified in IB. We’ll refactor to use a separate window controller later.
Also note that I ran this applet several times before the ‘Initial Window’ (set in IB) actually showed up when running the app; I’d file this under ‘Xcode bugs’.
Another problem with this default implementation is that every time you use a ‘show’ segue, you will get a new window (whether you link it to a view, or to a window). And since you also always get a new view controller, this is absolutely useless for… well, for just about any use I can think of. Why this is the default implementation, I do not know, but a process spawning multiple windows (which of course means that any information entered in the original window becomes moot and your desktop gets cluttered up with multiple instances of the same window) is pretty damn useless.
The trick is not completely obvious: in the attribute inspector of the target of the show segue, you have to set ‘Presentation’ from ‘Multiple’ (the default) to ‘Single’.
Beware: if the target is not a viewController, but your view has a window and a WindowController associated with it, setting the viewController’s presentation will not do anything – in that case, you need to set the WindowController’s Presentation mode.


1e) Create another view controller, call is ‘Sheet View Controller’, give it a ‘Popover Segue’ button, and connect the ‘Sheet Segue’ button to it. Choose ‘sheet’ for the segue type. Build and Run.

Sheets are document modal and need to be dismissed specifically; we don’t do that here, so you will find it impossible to quit the app other than by stopping it in Xcode. We will fix this later, too.
In fact, let’s fix it now. Add a ‘dismiss’ button to your sheet, and add the following action:
@IBAction func dismissSheet(_ sender: NSButton) {
dismissViewController(self)
}
At least a simple print statement placed after the dismissViewController call will get executed; I do not know whether this is always the case.
Since sheets are an interesting – but tangential – topic, I will not pursue them further in this place.
1f) Create the ‘Popover View Controller’ and add a ‘Modal Segue’ button; connect the sheet viewController with a ‘popover’ segue. Build and Run.

I’ve written about popovers and their customisation options earlier; so I won’t go into any detail here.
1g) Create the ‘Modal View Controller’ and give it a ‘Custom Segue’ button. Create a modal connection from the Popover Segue to this view controller. Build and Run.

You notice that the popover is dismissed once the user has interacted with it and opens another window. The modal window is functionally similar to the sheet; until the modal window is dismissed, the other windows cannot be activated (though they can be moved or resized). The modal window, however, can be dismissed using the ‘close’ button.
1h) Create a ‘Custom View Controller’ with a button saying ‘Show Tab ViewController’; create a segue from the previous view controller and choose ‘Custom’. Build and Run.
This will crash: Could not perform segue with identifier ‘(null)’. A segue must either have a performHandler or it must override -perform.
And thus, we have reached the point where we actually need to write code; everything else so far as been handled by the Storyboard and Apple’s default settings; maybe not to production standard (unnamed windows and an impossible-to-dismiss sheet), but good enough for prototyping.
(After restarting Xcode, I finally got warning: Unsupported Configuration: Custom Segue must specify a custom class derived from NSStoryboardSegue – this did not appear initially.)
2) Custom Segues
Let us take a tentative look at cutom segues. A little research (including Apple’s Documentation, the multi-segue project linked in this stackoverflow question and this segue example on github) shows that
– you need to subclass NSStoryboardSegue and override its perform function
– you need to (or at least needed to, as per this post) give your segue an identifier due to a Swift glitch
– inside the perform function, the source controller presents the destination controller using func presentViewController(_ viewController: NSViewController,
animator: NSViewControllerPresentationAnimator)
The documentation for this method offers three warnings:
The view controller you provide in this parameter must not already be visible elsewhere, or else this method raises an exception.
The view in the presented view controller must have a window, or else this method raises an exception.
[animator] must not be nil, or else this method raises an exception.
(and no, you cannot simply use NSViewControllerPresentationAnimator(); that class has no default initialisers and no default implementation).
I am also slightly confused about ‘must have a window’; in the above-linked, working example, the segue is to an NSViewController rather than to a NSWindowController with embedded ViewController.
But anyway. This is a shitload of work. You need a custom segue class, and a custom animator, which is another 15-20 lines of code, and while I’m of course perfectly capable of copying-and-pasting this, it would take me a couple of days to compare all available examples, form an opinion on best practices, and settle on an example I feel comfortable with. (Simply copying someone else’s code makes me extremely uncomfortable, even if the code turns out to be great: I hate using code I don’t understand.)
While a custom animation might be just what you need in your app, that seems to be the main reason FOR writing a custom segue. For my app – which, when all the code lived inside the AppDelegate had less than 50 lines of code in total – it seems decidedly overkill.
At some point I might play with this simply out of interest, but that point is not now.
The task I am trying to solve was ‘transition between view controllers in the same window’, so let’s explore other options of achieving the same result.
3) WindowController ContentController
Experiment: switching content controllers
I’d like to start with a warning: under 10.12/Xcode 9b3, this method came with two hiccups. In the first round my app remembered the window position, transitioned to the second controller, and crashed when trying to transition back to the initial controller, telling me that no such storyboard item existed (I had, of course, given it an identifyer and copied that identifier; but it didn’t want to stick and even vanished after a restart). After transitioning to a third controller and then changing it back to the initial one, it now DOES exist, but the window always opens in the middle of the screen, and if the app remembers the window position after startup, it snaps back into the centre of the screen regardless of where the window ought to appear (according to its initial position in IB) OR its last position. That bit – see above – used to work.
Therefore I offer this as a possible solution, but with the caveat that it might develop unwanted side effects.
3a) Create an UnrelatedViewController subclass of NSViewController, add a ‘Change’ button to it, and add the following IBAction:
@IBAction func changeVC(_ sender: Any) {
if let myWindowController = view.window?.windowController as? MainWindowController {
myWindowController.toggleContentViewController()
}
}
3b) Add the same IBAction to your initial ViewController.
3c) Create an NSWindowController subclass, set it as the class for the initial WindowController:
import Cocoa
class MainWindowController: NSWindowController {
var viewControllerType: ViewControllers = .initial
This is an enum included below; it has a case for every view controller we want to switch to (here, a choice of two) and includes a simple state machine for deciding which controller to switch to. (The simplest case: the one that isn’t current).
func toggleContentViewController(){
contentViewController = viewControllerType.nextController
viewControllerType = viewControllerType == .initial ? .unrelated : .initial
}
}
Since we have only two viewControllers, the ternary operator will do. The main challenge here is not to duplicate the logic by which the next state is chosen; I’ve fudged it because there ARE only two states and it’s bloody obvious, but this is not great code; in a better world I’d return a tuple of (firstViewController, .initial). This type of code is extremely error-prone: the logic should be in one place only. Here, for the sake of convenience, I am opting for a quick-and-dirty solution.
enum ViewControllers {
case initial
case unrelated
var nextController: NSViewController {
switch self {
case .initial:
let secondViewController = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "UnrelatedViewController")) as! NSViewController
return secondViewController
case .unrelated:
let firstViewController = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "FirstViewController")) as! NSViewController
return firstViewController
}
}
}
3d) Build and run. My experience – see above – is that this _should_ run, but _might not_, or at least not without side-effects, and I’m behind with my radars so will not build a new project and file it right now.
3e) You could, of course, add views as subviews; but there’s a reason we haven’t used that technique in this app: I want to use ViewController/View pairings, not just views; I do not want one viewController to handle several alternating views.
4) Let’s go back to “containment (rather than transition) is the more common notion for storyboards” and look at it again, and let’s work with the tools that Apple has baked into Xcode.
4a) Drag a TabViewController into the Storyboard, and show it (you should make it the initial viewController because we’ll want to use the NSWindowController subclass in a moment.). Customise the two tabs that it comes with.
Build and Run: hey presto, painless, animated, and very easy to set up view controller switching.
For the app I’m building ‘user can switch at any time’ is the wrong model. (In fact, once I’d gone through the list of available options, I started wondering whether a sheet mightn’t be just the ticket)
4b) In the TabViewController scene, change the style of the TabViewController to ‘Unspecified’.

Then select the TabView (I find the Scene list works better than trying to select with a mouse) and set ITS style to ‘tabless’.


The fact that there are three ‘tabless’ styles available give a clue as to how Apple thinks this will be used.
However. If you’re not using one of the inbuilt styles – tabs or toolbar – you’re losing the easy way of switching between tabs, so you need to find a different way to get back to your TabViewController.
4c) Create two NSViewControllerSubclasses, and assign them to your tab items. Add labels to their views so you can see which one is currently shown.
5) The first method (only because I encountered it first; I do not recommend using it) uses an NSWindowController to switch tabs. I’m leaving it here because it’s interesting. WindowControllers used to be much more common on macOS, back in the days of .XIB files (before storyboards; the only place I’m still using ‘nib files’ (an older name, after the older .nib format) is in NSCollectionView, which I have not been able to instantiate from storyboard.) Then the previously fairly useless NSViewController got a big update and got inserted into the responder chain, and storyboards happened; and people started using similar patterns to iOS where ViewControllers are everything.
These days, I mainly use WindowControllers to tidy things up; during development I ignore ‘what happens when the window is closed’ and almost always simply use ViewControllers without embedding them in WindowControllers.
So the following steps are more proof of concept than recommendation; it’s useful to have worked with WindowControllers, but I don’t feel this is the way forward. Just because something works doesn’t make it good. You could solve this problem in any number of ways – send notifications, use KVO, use a callback, create a delegate… but ‘making it work’ is not the point.
return of the NSWindowController
This tutorial does neat things with a segmented control in the toolbar, and is worth keeping around for that reason alone. I found more or less the same technique explained in several locations, which is why I tried it first.
5a) Set the TabViewController as the contentView of your MainWindowController and make the MainWindowController the entry point for the tab view. With this method, if the tabview is not embedded in the MainWindowController, it won’t work. It can’t.
5b) Add a ‘second’ and ‘first’ button to the first and second tab controller respectively. and add the following IBAction to both:
@IBAction func changeTab(_ sender: Any) {
if let myWindowController = view.window?.windowController as? MainWindowController {
myWindowController.toggleTabs()
}
}
5c) Add the ‘toggleTabs’ function to the MainWindowController class:
func toggleTabs(){
if let contentViewController = self.contentViewController as? NSTabViewController {
contentViewController.selectedTabViewItemIndex = contentViewController.selectedTabViewItemIndex == 0 ? 1 : 0
}
}
Again, since we only have two and always want to switch in sequence, the ternary operator is all the state machine logic we need.
Build and Run. Invisible tabs, animated switching between tabs, determined solely by your application’s logic.
5d) Break your app for fun and profit: go back to the UnrelatedViewController and press the ‘show Tab View Controller’ button. Your app will crash, because it tries to show the viewController in a standard NSWindowController window, but it’s embedded in a custom subclass thereof. If you redirect the ‘show’ segue to the MainWindowController, it’ll work again. I mainly wanted to write this down here because it’s such an easy mistake to make, and quite puzzling until you know where to look.
The error message was
*** Assertion failure in -[Storyboard_Segues.TabViewController addTabViewItem:], /Library/Caches/com.apple.xbs/Sources/AppKit/AppKit-1504.83.101/Controllers/NSTabViewController.m:581
2017-10-27 23:07:08.223482+0100 Storyboard Segues[1754:219104] Failed to set (tabViewItems) user defined inspected property on (Storyboard_Segues.TabViewController): Invalid parameter not satisfying: [tabViewItem viewController] != nil
which I did not find completely satisfying.
6) Earlier, I mentioned the responder chain. (Apple’s EventHandling Guide has much information, it’s also long and complex). The interesting bit isn’t that events (keystrokes, mouseclicks) get passed from one layer to another until someone handles them, but that every scene in InterfaceBuilder shows the ‘firstResponder’ item, which – aaargh – gives you access to every method ever. Do rightclick, and step away quietly – your app handles _a lot_ of methods, and they’re all there.
Obviously, getting your code in a twist isn’t a great idea, and you usually should stick to connecting controls in a view to the relevant viewController or, at most, a related NSWindowController.
But here, the responderChain works completely in our favour. (The other use case for connecting actions to firstResponder is your application’s menu; I tend not to bother during prototyping, but otherwise you would have no good way of connecting menu actions to the various part of your application.)
6a) Subclass NSTabViewController and assign the TabViewController to your TabView.
Add the following IBAction (same lazy code as before; I want to select one of two tabs, not write good tab-switching code)
@IBAction func tabControllerChangeTab(_ sender: Any) {
selectedTabViewItemIndex = selectedTabViewItemIndex == 0 ? 1 : 0
}
6b) Add ‘switch via firstResponder’ buttons to both tab views, and connect them to the firstResponder’s ‘tabControllerChangeTab’ function.
6c) Build and Run. Not only does this work every bit as smoothly as before, not only does it need even less code than the windowController, version, but your logic for ‘when to switch tabs and what to do when you switch’ now resides solely within the tabController class, where it belongs, and you can check which tab item is currently active, suck any data from it that you want to pass on, and switch to any tab you fancy, passing in any information that is needed there.
6d) This is where good naming practice comes in, because in a complex app you will get _a lot_ of methods, and you don’t want to connect to the wrong one. Here, with one TabViewController, ‘changeTab’ might be ok; but in a larger app, better naming is mandatory.
In my example app, ‘changeTab’ was NOT ok. That’s because I had already named the actions in the two tabs themselves ‘changeTab’ and when you connect to firstResponder, you connect to the nearest method. It worked smoothly… until I commented out the superfluous code which broke everything. Ooops. Lesson learnt: when I want to use the responder chain, I will give my methods unambiguous names.
6e) Step out of the responder chain: Add a button to the unrelatedViewController, set the segue of the ‘show TabViewController’ button to show the MainWindowController, and connect your new button to the ‘changeTab’ action in FirstResponder.]
Build and run, open the tab view, and press your new button.
Nothing happens, because this viewController is not in the same hierarchy as the tab view. They’re, for want of a better word, siblings (or cousins once removed?), but you cannot use this mechanism to talk to an unrelated class.
7) For the sake of completion, there’s also a solution involving Container views, but since this duplicates the functionality of a tab view by messing with segues, and without any of the benefits, I’m not going to touch it here. ContainerViews are great for modularizing your code. A containerView has an embedSegue to another viewController which you can use to pass data to the container content, but that’s outside the scope of this post.
And here endeth the tour of segues. At some point I will add an example of an embed segue, since I think they’re a good way to combat MassiveViewControllers, and it’s possible that I’ll look at customSegues and their custom animation again, but for now, until Apple changes its implementation of storyboards, I am happy with this post.
Oct 28 2017
macOS Storyboard Segues
The other day I had what I thought a very simple request: I wanted to replace one viewcontroller with another in the same window. This is something that on iOS (particularly iPhones) happens all the time; but somewhat to my suprise, macOS does not have an obvious out-of-the-box solution for this.
This turns out to be a much more interesting question than it seemed at first because it quickly arrives at architecture and best practice: the question is less how to do this (there are several ways), but _whether_.
The NSStoryboard documentation says ‘in macOS, containment (rather than transition) is the more common notion for storyboards‘ which gives an intrigueing hint; we will explore this further once we have dealt with the common and/or obvious segues.
Since we’re here, I thought I’d write up all of the storyboard segues so I don’t have to do this again any time soon.
All instructions are for Xcode 9b3, Swift 4, OS 10.12.
standard segues
1a) Add a label (‘Initial View Controller’ and a button (‘Show Segue’) to the initial view controller. Name the window ‘Initial Window’

1b) Create a second view controller. Give it a label (‘Show ViewController’) and a button (‘Sheet Segue’).
1c) Drag from the ‘show segue’ button to the new view controller; choose ‘show’ from the menu.
(the ‘segue’ coming in is the ‘content view controller’ relationship automatically set up by Xcode).
1d) Build and Run.
You notice that the second view controller is shown in a new window, but since we haven’t specified a window controller for it, it is merely generic and has no title. You will also notice that while the app saves the location of the original window, this one will open up at the coordinates specified in IB. We’ll refactor to use a separate window controller later.
Also note that I ran this applet several times before the ‘Initial Window’ (set in IB) actually showed up when running the app; I’d file this under ‘Xcode bugs’.
Another problem with this default implementation is that every time you use a ‘show’ segue, you will get a new window (whether you link it to a view, or to a window). And since you also always get a new view controller, this is absolutely useless for… well, for just about any use I can think of. Why this is the default implementation, I do not know, but a process spawning multiple windows (which of course means that any information entered in the original window becomes moot and your desktop gets cluttered up with multiple instances of the same window) is pretty damn useless.
The trick is not completely obvious: in the attribute inspector of the target of the show segue, you have to set ‘Presentation’ from ‘Multiple’ (the default) to ‘Single’.
Beware: if the target is not a viewController, but your view has a window and a WindowController associated with it, setting the viewController’s presentation will not do anything – in that case, you need to set the WindowController’s Presentation mode.
1e) Create another view controller, call is ‘Sheet View Controller’, give it a ‘Popover Segue’ button, and connect the ‘Sheet Segue’ button to it. Choose ‘sheet’ for the segue type. Build and Run.

Sheets are document modal and need to be dismissed specifically; we don’t do that here, so you will find it impossible to quit the app other than by stopping it in Xcode. We will fix this later, too.
In fact, let’s fix it now. Add a ‘dismiss’ button to your sheet, and add the following action:
@IBAction func dismissSheet(_ sender: NSButton) {
dismissViewController(self)
}
At least a simple print statement placed after the dismissViewController call will get executed; I do not know whether this is always the case.
Since sheets are an interesting – but tangential – topic, I will not pursue them further in this place.
1f) Create the ‘Popover View Controller’ and add a ‘Modal Segue’ button; connect the sheet viewController with a ‘popover’ segue. Build and Run.

I’ve written about popovers and their customisation options earlier; so I won’t go into any detail here.
1g) Create the ‘Modal View Controller’ and give it a ‘Custom Segue’ button. Create a modal connection from the Popover Segue to this view controller. Build and Run.

You notice that the popover is dismissed once the user has interacted with it and opens another window. The modal window is functionally similar to the sheet; until the modal window is dismissed, the other windows cannot be activated (though they can be moved or resized). The modal window, however, can be dismissed using the ‘close’ button.
1h) Create a ‘Custom View Controller’ with a button saying ‘Show Tab ViewController’; create a segue from the previous view controller and choose ‘Custom’. Build and Run.
This will crash: Could not perform segue with identifier ‘(null)’. A segue must either have a performHandler or it must override -perform.
And thus, we have reached the point where we actually need to write code; everything else so far as been handled by the Storyboard and Apple’s default settings; maybe not to production standard (unnamed windows and an impossible-to-dismiss sheet), but good enough for prototyping.
(After restarting Xcode, I finally got warning: Unsupported Configuration: Custom Segue must specify a custom class derived from NSStoryboardSegue – this did not appear initially.)
2) Custom Segues
Let us take a tentative look at cutom segues. A little research (including Apple’s Documentation, the multi-segue project linked in this stackoverflow question and this segue example on github) shows that
– you need to subclass NSStoryboardSegue and override its perform function
– you need to (or at least needed to, as per this post) give your segue an identifier due to a Swift glitch
– inside the perform function, the source controller presents the destination controller using func presentViewController(_ viewController: NSViewController,
animator: NSViewControllerPresentationAnimator)
The documentation for this method offers three warnings:
The view controller you provide in this parameter must not already be visible elsewhere, or else this method raises an exception.
The view in the presented view controller must have a window, or else this method raises an exception.
[animator] must not be nil, or else this method raises an exception.
(and no, you cannot simply use NSViewControllerPresentationAnimator(); that class has no default initialisers and no default implementation).
I am also slightly confused about ‘must have a window’; in the above-linked, working example, the segue is to an NSViewController rather than to a NSWindowController with embedded ViewController.
But anyway. This is a shitload of work. You need a custom segue class, and a custom animator, which is another 15-20 lines of code, and while I’m of course perfectly capable of copying-and-pasting this, it would take me a couple of days to compare all available examples, form an opinion on best practices, and settle on an example I feel comfortable with. (Simply copying someone else’s code makes me extremely uncomfortable, even if the code turns out to be great: I hate using code I don’t understand.)
While a custom animation might be just what you need in your app, that seems to be the main reason FOR writing a custom segue. For my app – which, when all the code lived inside the AppDelegate had less than 50 lines of code in total – it seems decidedly overkill.
At some point I might play with this simply out of interest, but that point is not now.
The task I am trying to solve was ‘transition between view controllers in the same window’, so let’s explore other options of achieving the same result.
3) WindowController ContentController
Experiment: switching content controllers
Therefore I offer this as a possible solution, but with the caveat that it might develop unwanted side effects.
3a) Create an UnrelatedViewController subclass of NSViewController, add a ‘Change’ button to it, and add the following IBAction:
@IBAction func changeVC(_ sender: Any) {
if let myWindowController = view.window?.windowController as? MainWindowController {
myWindowController.toggleContentViewController()
}
}
3b) Add the same IBAction to your initial ViewController.
3c) Create an NSWindowController subclass, set it as the class for the initial WindowController:
import Cocoa
class MainWindowController: NSWindowController {
var viewControllerType: ViewControllers = .initial
This is an enum included below; it has a case for every view controller we want to switch to (here, a choice of two) and includes a simple state machine for deciding which controller to switch to. (The simplest case: the one that isn’t current).
func toggleContentViewController(){
contentViewController = viewControllerType.nextController
viewControllerType = viewControllerType == .initial ? .unrelated : .initial
}
}
Since we have only two viewControllers, the ternary operator will do. The main challenge here is not to duplicate the logic by which the next state is chosen; I’ve fudged it because there ARE only two states and it’s bloody obvious, but this is not great code; in a better world I’d return a tuple of (firstViewController, .initial). This type of code is extremely error-prone: the logic should be in one place only. Here, for the sake of convenience, I am opting for a quick-and-dirty solution.
enum ViewControllers {
case initial
case unrelated
var nextController: NSViewController {
switch self {
case .initial:
let secondViewController = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "UnrelatedViewController")) as! NSViewController
return secondViewController
case .unrelated:
let firstViewController = NSStoryboard.init(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier(rawValue: "FirstViewController")) as! NSViewController
return firstViewController
}
}
}
3d) Build and run. My experience – see above – is that this _should_ run, but _might not_, or at least not without side-effects, and I’m behind with my radars so will not build a new project and file it right now.
3e) You could, of course, add views as subviews; but there’s a reason we haven’t used that technique in this app: I want to use ViewController/View pairings, not just views; I do not want one viewController to handle several alternating views.
4) Let’s go back to “containment (rather than transition) is the more common notion for storyboards” and look at it again, and let’s work with the tools that Apple has baked into Xcode.
4a) Drag a TabViewController into the Storyboard, and show it (you should make it the initial viewController because we’ll want to use the NSWindowController subclass in a moment.). Customise the two tabs that it comes with.
Build and Run: hey presto, painless, animated, and very easy to set up view controller switching.
For the app I’m building ‘user can switch at any time’ is the wrong model. (In fact, once I’d gone through the list of available options, I started wondering whether a sheet mightn’t be just the ticket)
4b) In the TabViewController scene, change the style of the TabViewController to ‘Unspecified’.
Then select the TabView (I find the Scene list works better than trying to select with a mouse) and set ITS style to ‘tabless’.


The fact that there are three ‘tabless’ styles available give a clue as to how Apple thinks this will be used.
However. If you’re not using one of the inbuilt styles – tabs or toolbar – you’re losing the easy way of switching between tabs, so you need to find a different way to get back to your TabViewController.
4c) Create two NSViewControllerSubclasses, and assign them to your tab items. Add labels to their views so you can see which one is currently shown.
5) The first method (only because I encountered it first; I do not recommend using it) uses an NSWindowController to switch tabs. I’m leaving it here because it’s interesting. WindowControllers used to be much more common on macOS, back in the days of .XIB files (before storyboards; the only place I’m still using ‘nib files’ (an older name, after the older .nib format) is in NSCollectionView, which I have not been able to instantiate from storyboard.) Then the previously fairly useless NSViewController got a big update and got inserted into the responder chain, and storyboards happened; and people started using similar patterns to iOS where ViewControllers are everything.
These days, I mainly use WindowControllers to tidy things up; during development I ignore ‘what happens when the window is closed’ and almost always simply use ViewControllers without embedding them in WindowControllers.
So the following steps are more proof of concept than recommendation; it’s useful to have worked with WindowControllers, but I don’t feel this is the way forward. Just because something works doesn’t make it good. You could solve this problem in any number of ways – send notifications, use KVO, use a callback, create a delegate… but ‘making it work’ is not the point.
return of the NSWindowController
5a) Set the TabViewController as the contentView of your MainWindowController and make the MainWindowController the entry point for the tab view. With this method, if the tabview is not embedded in the MainWindowController, it won’t work. It can’t.
5b) Add a ‘second’ and ‘first’ button to the first and second tab controller respectively. and add the following IBAction to both:
@IBAction func changeTab(_ sender: Any) {
if let myWindowController = view.window?.windowController as? MainWindowController {
myWindowController.toggleTabs()
}
}
5c) Add the ‘toggleTabs’ function to the MainWindowController class:
func toggleTabs(){
if let contentViewController = self.contentViewController as? NSTabViewController {
contentViewController.selectedTabViewItemIndex = contentViewController.selectedTabViewItemIndex == 0 ? 1 : 0
}
}
Again, since we only have two and always want to switch in sequence, the ternary operator is all the state machine logic we need.
Build and Run. Invisible tabs, animated switching between tabs, determined solely by your application’s logic.
5d) Break your app for fun and profit: go back to the UnrelatedViewController and press the ‘show Tab View Controller’ button. Your app will crash, because it tries to show the viewController in a standard NSWindowController window, but it’s embedded in a custom subclass thereof. If you redirect the ‘show’ segue to the MainWindowController, it’ll work again. I mainly wanted to write this down here because it’s such an easy mistake to make, and quite puzzling until you know where to look.
The error message was
*** Assertion failure in -[Storyboard_Segues.TabViewController addTabViewItem:], /Library/Caches/com.apple.xbs/Sources/AppKit/AppKit-1504.83.101/Controllers/NSTabViewController.m:581
2017-10-27 23:07:08.223482+0100 Storyboard Segues[1754:219104] Failed to set (tabViewItems) user defined inspected property on (Storyboard_Segues.TabViewController): Invalid parameter not satisfying: [tabViewItem viewController] != nil
which I did not find completely satisfying.
6) Earlier, I mentioned the responder chain. (Apple’s EventHandling Guide has much information, it’s also long and complex). The interesting bit isn’t that events (keystrokes, mouseclicks) get passed from one layer to another until someone handles them, but that every scene in InterfaceBuilder shows the ‘firstResponder’ item, which – aaargh – gives you access to every method ever. Do rightclick, and step away quietly – your app handles _a lot_ of methods, and they’re all there.
Obviously, getting your code in a twist isn’t a great idea, and you usually should stick to connecting controls in a view to the relevant viewController or, at most, a related NSWindowController.
But here, the responderChain works completely in our favour. (The other use case for connecting actions to firstResponder is your application’s menu; I tend not to bother during prototyping, but otherwise you would have no good way of connecting menu actions to the various part of your application.)
6a) Subclass NSTabViewController and assign the TabViewController to your TabView.
Add the following IBAction (same lazy code as before; I want to select one of two tabs, not write good tab-switching code)
@IBAction func tabControllerChangeTab(_ sender: Any) {
selectedTabViewItemIndex = selectedTabViewItemIndex == 0 ? 1 : 0
}
6b) Add ‘switch via firstResponder’ buttons to both tab views, and connect them to the firstResponder’s ‘tabControllerChangeTab’ function.
6c) Build and Run. Not only does this work every bit as smoothly as before, not only does it need even less code than the windowController, version, but your logic for ‘when to switch tabs and what to do when you switch’ now resides solely within the tabController class, where it belongs, and you can check which tab item is currently active, suck any data from it that you want to pass on, and switch to any tab you fancy, passing in any information that is needed there.
6d) This is where good naming practice comes in, because in a complex app you will get _a lot_ of methods, and you don’t want to connect to the wrong one. Here, with one TabViewController, ‘changeTab’ might be ok; but in a larger app, better naming is mandatory.
In my example app, ‘changeTab’ was NOT ok. That’s because I had already named the actions in the two tabs themselves ‘changeTab’ and when you connect to firstResponder, you connect to the nearest method. It worked smoothly… until I commented out the superfluous code which broke everything. Ooops. Lesson learnt: when I want to use the responder chain, I will give my methods unambiguous names.
6e) Step out of the responder chain: Add a button to the unrelatedViewController, set the segue of the ‘show TabViewController’ button to show the MainWindowController, and connect your new button to the ‘changeTab’ action in FirstResponder.]
Build and run, open the tab view, and press your new button.
Nothing happens, because this viewController is not in the same hierarchy as the tab view. They’re, for want of a better word, siblings (or cousins once removed?), but you cannot use this mechanism to talk to an unrelated class.
7) For the sake of completion, there’s also a solution involving Container views, but since this duplicates the functionality of a tab view by messing with segues, and without any of the benefits, I’m not going to touch it here. ContainerViews are great for modularizing your code. A containerView has an embedSegue to another viewController which you can use to pass data to the container content, but that’s outside the scope of this post.
And here endeth the tour of segues. At some point I will add an example of an embed segue, since I think they’re a good way to combat MassiveViewControllers, and it’s possible that I’ll look at customSegues and their custom animation again, but for now, until Apple changes its implementation of storyboards, I am happy with this post.
By Extelligent Cocoa • Application Design, Design Patterns, Interface, Wiki • • Tags: NSStoryboardSegue, NSViewController, NSWindowController, storyboards