NSPopover and storyboards

macOS 10.12, Xcode 8.0, Swift 3

Storyboard segues make it exceedingly easy to create popovers, but sometimes you want to customise your popovers a bit more, for instance make windows detachable.

When building interfaces the old-fashioned way with xib files or in code, you would use the NSPopoverDelegate protocol and wire things up appropriately.

Storyboards present a unique challenge here: on the one hand, you get the delegate automatically, on the other hand, each time the popover is opened you get a new viewController, and thus a new delegate, which means you cannot save its state.
0) Basic setup
a) set up a new project with storyboards: place a button in your main view controller with the title ‘openPopover’, drag another viewController into the storyboard, put a label into it so it’ll be recognisable, and create a popover Segue.
Proof of concept: Build and run. Instant popover.

b) create a new subclass of NSViewController and assign it to the popover contents. Create an outlet for that view’s label.

[code language=”plain”]class PopoverController: NSViewController {

override func viewDidLoad() {
super.viewDidLoad()
label.stringValue = “I’m a popover”
}

@IBOutlet weak var label: NSTextField!
}[/code]

 

1) Creating an NSPopoverDelegate

The key lies in the following method on NSViewController which gets called by the segue:

[code language=”plain”]func presentViewController(_ viewController: NSViewController,
asPopoverRelativeTo positioningRect: NSRect,
of positioningView: NSView,
preferredEdge: NSRectEdge,
behavior: NSPopoverBehavior)[/code]

 
The following line in the documentation describes how popoverDelegates are handled in storyboards:

The presented view controller is the delegate and the content view controller of the popover. You can use NSPopoverDelegate methods to customize the popover.

 

This means that if you declare the ContentViewController class to conform to NSPopoverDelegate, it will automatically be set as the popover’s delegate even in a popover segue; you do not need to call the function above in code or set the delegate manually, as you had to when instantiating a .xib-based popover.

a) Declare the PopoverViewController to conform to NSPopoverDelegate

In order to detach the popover, you simply add

[code language=”plain”] func popoverShouldDetach(_ popover: NSPopover) -> Bool {
return true
}[/code]

 

to the popover’s contentViewController – you get a generic window with the ViewController embedded within. (This is a new method in Yosemite.)

Build and run. You can now detach the window, and call up a popover, and detach a window, and repeat until you run out of memory. This is not ideal.

more complex than expected thanks to the Storyboard habit of instantiating a new ViewController every time you show a popover.

2) My first impulse was to set a tracking variable to determine whether the popover had been detached, but since every attempt to detach a new popover creates a new instance of ContentViewController, popoverShouldDetach can only be used for that logic if you override performSegue and pass in an appropriate value for that from the initial viewController or if you override presentViewController, which is the option I have chosen here.

The problem here is that the popover can have three states, and each state has two behaviours associated with it:
– case setup (the popover is not shown)
– case attached(the popover is shown as a popover)
– case detached (the popover is shown as a separate window)

In the first case, you want to show the popover and don’t care about detaching: there’s nothing to detach
In the second, you want to be able to detach it, but not show it again (which won’t work anyway since the behaviour is transient; by default, the app will simply close the popover)
In the third, you don’t want to show the popover again, and detach is a moot question since there’s nothing TO detach

The solution I am proposing here is slight overkill: I have an enum with three cases and two boolean variables, shouldShow and shouldDetach. This is, to a great degree, so that my intent is obvious: I could have gotten away with a single boolean and some juggling, but this way, the code is much clearer, and if I ever want to change the behaviour, I can expand it more easily. (A possible use pattern is to show only the barebones content in the popover, but allow detaching as a pallette with additional options; in that case, the ‘show’ pattern would be different.)

a) Create the enum:

[code language=”plain”]enum PopoverState {
case setup
case attached
case detached

var shouldDetach: Bool {
switch self {
case .setup:
return true
case .attached:
return true
case .detached:
return false
}
}

var shouldShow: Bool {
switch self {
case .setup:
return true
case .attached:
return false
case .detached:
return false
}
}

}[/code]

 

b) Add

[code language=”plain”]var popoverState: PopoverState = .setup
[/code]

 

to the ViewController class.

c) Create a function that changes the popoverState:

[code language=”plain”] func changePopoverState(to state: PopoverState) -> () {
popoverState = state
}[/code]

 

This function has the type PopoverState -> () – we will pass it as a completionHandler to our PopoverViewController in a moment.

d) override presentViewController:

[code language=”plain”] override func presentViewController(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopoverBehavior) {
if let viewController = viewController as? PopoverController{
viewController.shouldDetach = popoverState.shouldDetach
viewController.completionHandler = changePopoverState
}
if popoverState.shouldShow == true{
super.presentViewController(viewController, asPopoverRelativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge, behavior: behavior)
}
}[/code]

 

Here we are using both of the variables discussed above: we’re setting the popoverController’s ‘shouldDetach’ variable to the correct setting, and if we should show the popover, we do so.

e) Practice good citizenship

Above, I’m passing in a named function as completion handler, which means that my PopoverController now retains a reference to the ViewController. For this app, this is not a problem – the PopoverController’s are very transient and there’s no use case for closing the main window and leaving the popover open. My main concern here is readability: rather than puzzling out closure syntax, it should be very obvious what this function does. In production code, this should be replaced with

viewController.completionHandler = { [unowned self] (state: PopoverState) -> Void in self.popoverState = state }

This is a thing I will do frequently during coding: write a named function to pass as completion handler and only turn it into a closure once I am confident that the function and its logic work exactly as I want them to. For me, closures have a much higher cognitive load than functions, and so I avoid wrestling with closure syntax until other things are worked out.
f) set up the PopoverController class. In the step above we promised two properties, now we implement them:

[code language=”plain”] var shouldDetach: Bool = true
var completionHandler: ((PopoverState) -> ())?[/code]

 

(There’s no good way of ordering these steps. Normally, I’d add the variables to the target class before setting them in a prepareForSegue or other method, but here I wanted to avoid jumping between the two classes too much.)

g) based on shouldDetach, we can now implement popoverShouldDetach properly:

[code language=”plain”] func popoverShouldDetach(_ popover: NSPopover) -> Bool {
return shouldDetach
}[/code]

 

h) Going from presenting viewController to the viewController being presented is easy, but it’s a one-way street. We need a way to return the correct state of the popover. Here, I’m making use of a completion handler (see c and e above) rather than using delegation; but that is partly a question of style. I prefer delegation when the idiom is ‘delegate.doSomethingForMe’ (= calling methods) and callbackHandlers when I want to pass information back (=this state has changed).

The NSPopoverDelegate protocol contains a number of state-change notifications. popoverDidClose, popoverDidShow and popoverDidDetach correspond nicely to the states we’re interested in:

[code language=”plain”] func popoverDidClose(_ notification: Notification) {
completionHandler?(.setup)
}

func popoverDidShow(_ notification: Notification) {
completionHandler?(.attached)
}

func popoverDidDetach(_ popover: NSPopover) {
completionHandler?(.detached)
}
[/code]

 

Build and Run.

This is a workable implementation. The popover shows only once, detaches when asked to, and shows again once the detached window is closed.

But it’s also slightly awkward, because the popover button remains active even when it’s not doing anything.

3) Tidy things up:

Add an outlet for the ‘Show Popover’ button

@IBOutlet weak var showBtn: NSButton!

Add a little sugar to the popoverState variable:

[code language=”plain”] var popoverState: PopoverState = .setup{
didSet{
switch popoverState {
case .setup:
showBtn.title = “Show Popover”
showBtn.isEnabled = true
case .attached:
showBtn.title = “Hide Popover”
showBtn.isEnabled = true
case .detached:
showBtn.title = “Show Popover”
showBtn.isEnabled = false
}
}
}[/code]

 

Build and Run. Now the button tells you what it will do, and it’s only enabled when you can logically interact with it.


And more…

Usage Example

Any time you want to keep your interface uncluttered and expand it on demand, particularly for ‘quick’ operations – adding a couple of values; a preview of data.

Alternatives

Presentations of transient information:
– tooltips (text that shows up when the cursor enters an area but which cannot be interacted with)
– transient views which show information and vanish when the user clicks on them or elsewhere
– Popovers (content inside view can be interacted with)
– tabs (main content reduced to a line of identifiers; views cannot be displayed concurrently)
– disclosure triangles (which insert additional views to give more detailed information); users mostly decide the state depending on individual current needs.
– window drawers (in my experience users mostly either want access to the tools in the drawer or don’t require it; Apple now discourages their use)
– ‘advanced’ setup options where a different view controller manages a more detailed interface
– split view where the second view contains additional tools/interface details
– a customisable interface where users can decide which modules they want to view and where they want to locate them within a window

(This is probably not an exhaustive list).

Extensions

The more interesting method in the NSPopoverDelegate protocol is func detachableWindow(for popover: NSPopover) -> NSWindow?
This allows you to specify a window (and related NSWindowController) for the popover. Instead of the minimalist non-resizable window seen above, you can customize it to your heart’s content, for instance turn it into a panel (a common use of popovers).