When a window closes as soon as it has opened

A couple of days ago, my normally peaceful macOS development became quite frenetic: Podofyllin, my PDF viewer, wasn’t opening windows properly when running in Sierra, although they worked fine when tested here in Mojave. In fact, it wasn’t that the windows didn’t open properly: they opened, then in the blink of an eye vanished again.

Was it my code, or AppKit?

Podophyllin opens three different types of windows, beyond its standard About… window:

  • Regular NSDocument document windows, designed using Interface Builder, stored in a Nib, and opened by AppKit when a document is opened. Those were behaving fine.
  • Ancillary windows, also stored in a Nib, opened from menu commands in the Window menu, which are handled by Actions in NSDocument. Although these worked fine when tested in Mojave, and didn’t even report any issues, they were fleeting in Sierra.
  • A custom Help window, stored in a Nib, opened from a menu command in the AppDelegate and handled by an Action there. These too were fine in Mojave but broken in Sierra.

Tackling the last of these first, what my code needed to do was to instantiate an NSWindowController from the Storyboard and show it as being associated with the AppDelegate. There isn’t a great deal of code which might do that, and a fairly standard approach would be
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let theHelpWindow = (storyboard.instantiateController(withIdentifier: "Help Window Controller") as! NSWindowController)
theHelpWindow.showWindow(self)

At which point, the window should appear, but not vanish again. For Sierra though, the NSWindowController needs to be stored in an array, so that it doesn’t vanish. I therefore added a suitable empty array to the AppDelegate class
var myHelpWindow: [NSWindowController?] = []

And the Action to load and open the window becomes:
@IBAction func openHelp(_ sender: NSMenuItem) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let theHelpWindow = (storyboard.instantiateController(withIdentifier: "Help Window Controller") as! NSWindowController)
theHelpWindow.showWindow(self)
self.myHelpWindow.append(theHelpWindow)
}

Miss that last line out, and Sierra isn’t prepared to let the window hang around for more than the twinkling of an eye.

The situation is different with a window for a document. They need to be firmly attached, so that the window controller knows its document, and its document knows its list of window controllers. In most cases including mine, the view controller needs to be able to reference the document which owns it to access its data. The call to achieve that is NSDocument.addWindowController(), which should do both, and make the window ready for display.

The Action triggered by the menu command, in NSDocument, is therefore:
@IBAction func openPlainPDF(_ sender: Any) {
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = (storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("PlainPDFWindowController")) as! NSWindowController)
self.addWindowController(windowController)
windowController.showWindow(self)
}

podofyllin399

This needs to proceed in this order, and without any other code trying to set the window controller’s document. For once, Apple’s documentation is clear and informative:
“If you create window controllers in makeWindowControllers() or in any other context, such as in apps that present multiple windows per document, you should invoke this method for each window controller created.”

The logical sequence is to create the window controller from the Nib, add it to the document using NSDocument.addWindowController(), then call the window controller to show the window, as above. Mojave is more tolerant of variation in this, but for Sierra departing from that sequence can have odd effects, like showing the window momentarily, then vanishing it away. You can hardly call it a bug in AppKit, but it does require careful coding.

Postscript

Thanks to Jeff Johnson @lapcatsoftware who has not only already documented this behaviour and explained it, but discovered that Apple changed this in AppKit for 10.13, according to its release notes. We’re both puzzled that this fix doesn’t seem to have propagated back to Sierra, though, for apps which are built on later versions of the SDK. So long as you take precautions, as shown above, it shouldn’t trouble your apps.