33

I am trying to start a new document based Cocoa project in Swift and want to create a subclass of NSWindowController (as recommended in Apple's guides on document based apps). In ObjC you would make an instance of an NSWindowController subclass sending the initWithWindowNibName: message, which was implemented accordingly, calling the superclasses method.

In Swift init(windowNibName) is only available as an convenience initializer, the designated initializer of NSWindowController is init(window) which obviously wants me to pass in a window.

I cannot call super.init(windowNibName) from my subclass, because it is not the designated initializer, so I obviously have to implement convenience init(windowNibName), which in turn needs to call self.init(window). But if all I have is my nib file, how do I access the nib file's window to send to that initializer?

Martin
  • 333
  • 1
  • 3
  • 6

5 Answers5

22

Instead of overriding any of the init methods you can simply override the windowNibName property and return a hardcoded string. This allows you to call the basic vanilla init method to create the window controller.

class WindowController: NSWindowController {

    override var windowNibName: String! {
        return "NameOfNib"
    }
}

let windowController = WindowController()

I prefer this over calling let windowController = WindowController(windowNibName: "NameOfNib") as the name of the nib is an implementation detail that should be fully encapsulated within the window controller class and never exposed outside (and it's just plain easier to call WindowController()).

If you want to add additional parameters to the init method do the following:

  • In your custom init method call super.init(window: nil). This will get NSWindowController to init with the windowNibName property.
  • Override the required init(coder: NSCoder) method to either configure your object or simply call fatalError() to prohibit its use (albiet at runtime).
class WindowController: NSWindowController {

    var test: Bool

    override var windowNibName: String! {
        return "NameOfNib"
    }

    init(test: Bool) {
        self.test = test
        super.init(window: nil) // Call this to get NSWindowController to init with the windowNibName property
    }

    // Override this as required per the class spec
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented. Use init()")

        // OR

        self.test = false
        super.init(coder: coder)
    }
}

let windowController = WindowController(test: true)
ospr
  • 1,650
  • 2
  • 17
  • 21
  • Actually Apple themselves say that name of NIB is an implementation detail and should not be required but they way they describe how to subclass NSWindowController to archive that only works with Obj-C and totally fails with Swift. See yourself: https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/KeyObjects/KeyObjects.html Look for **An NSWindowController Subclass Manages Nib Files** – Mecki Apr 04 '19 at 17:27
  • 1
    Overriding `windowNibName` is now how Apple recommends that you subclass `NSWindowController` in swift. Check out the "Subclassing NSWindowController" section here. https://developer.apple.com/documentation/appkit/nswindowcontroller – macshome Jun 05 '20 at 17:22
17

You need to override either all three designated initializers of NSWindowController (init(), init(window) and init(coder)), or none of them, in which case your subclass will automatically inherit init(windowNibName) and all others convenience initializers and you will be able to construct it using superclass's convenience initializer:

// this overrides none of designated initializers
class MyWindowController: NSWindowController {
    override func windowDidLoad() {
        super.windowDidLoad()
    }
}

// this one overrides all of them
//
// Awkwardly enough, I see only two initializers 
// when viewing `NSWindowController` source from Xcode, 
// but I have to also override `init()` to make these rules apply.
// Seems like a bug.
class MyWindowController: NSWindowController
{
    init()
    {
        super.init()
    }

    init(window: NSWindow!)
    {
        super.init(window: window)
    }

    init(coder: NSCoder!)
    {
        super.init(coder: coder)
    }

    override func windowDidLoad() {
        super.windowDidLoad()
    }
}

// this will work with either of the above
let mwc: MyWindowController! = MyWindowController(windowNibName: "MyWindow")

This is covered by "Initialization / Automatic Initializer Inheritance" in the language guide:

However, superclass initializers are automatically inherited if certain conditions are met. In practice, this means that you do not need to write initializer overrides in many common scenarios, and can inherit your superclass initializers with minimal effort whenever it is safe to do so.

Assuming that you provide default values for any new properties you introduce in a subclass, the following two rules apply:

Rule 1 If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.

Rule 2 If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.

hamstergene
  • 24,039
  • 5
  • 57
  • 72
  • 2
    Be sure to set nib's FileOwner to use the custom NSWindowController class in the identity inspector, drag the window outlet to the window and then store the custom NSWindowController instance as a property on a class, otherwise it gets garbage collected. – Bjorn Jan 03 '15 at 02:33
  • The funny thing is, that is totally incompatible to what Apple says how you shall sub-class `NSWindowsController` in Obj-C. See https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/KeyObjects/KeyObjects.html and go to **An NSWindowController Subclass Manages Nib Files** – Mecki Apr 04 '19 at 17:15
5

I was able to work around this by just having a class method that calls the convenience initializer, modifies the custom variables, then returns the new object.

So an Objective C init method would look like this:

//Class.h
@class PPPlugInInfo;

@interface PPPlugInInfoController : NSWindowController
//...
- (id)initWithPlugInInfo:(PPPlugInInfo *)plugInfo;
//...
@end

//Class.m
#include "Class.h"

@interface PPPlugInInfoController ()
@property (strong) PPPlugInInfo *info;
@end

@implementation PPPlugInInfoController

- (id)initWithPlugInInfo:(PPPlugInInfo *)plugInfo;
{
    if (self = [self initWithWindowNibName:@"PPPlugInInfoController"]) {
        self.info = plugInfo;
    }
    return self;
}

@end

This is how I did the Swift version:

class PPPluginInfoController: NSWindowController {
    private var info: PPPlugInInfo!
    class func windowControllerFromInfo(plugInfo: PPPlugInInfo) -> Self {
        var toRet = self(windowNibName:"PPPlugInInfoController")
        toRet.info = plugInfo

        return toRet
    }
}
MaddTheSane
  • 2,981
  • 24
  • 27
  • 1
    Great idea, this definitely seems like the best option. Although I hate that is ends up making non-optional properties (var info) optional though – logancautrell Sep 14 '14 at 17:51
  • I did find this post, and (at least in this case) overriding windowNibName addresses this issue. http://dev.eltima.com/post/91454912064/nswindowcontroller-subclass-in-swift-project – logancautrell Sep 14 '14 at 17:59
1

The stroke of genius in @hamstergene's answer is to override init() as well, which is inherited from NSResponder. Now one can introduce a new initialiser and delegate to self.init(windowNibName: NoteWindowName), which is in turn inherited once all three designated initialisers are overridden:

class WindowController: NSWindowController {

    var note: Document! // must be optional because self is not available before delegating to designated init

    convenience init(note: Document) {
        self.init(windowNibName: NoteWindowName)
        self.document = document
    }

    override init(window: NSWindow?) {
        super.init(window: window)
    }

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

    override init() {
        fatalError("init() has not been implemented")
    }
}

Now it is no longer necessary to tell the custom window controller what nib file to load from. Instead, it can be specialised for whatever motivated the subclass in the first place (like taking part in some document hierarchy, for example)...

Milos
  • 2,728
  • 22
  • 24
-1

An update to hamstergene answer.

This works fine on Xcode Version 6.1 (6A1052d)

Add your custom class to window controller

//
//  MainWindowController.swift
//  VHDA Editor
//
//  Created by Holyfield on 20/11/14.
//  Copyright (c) 2014 Holyfield. All rights reserved.
//

import Cocoa

class MainWindowController: NSWindowController {

    //override func windowDidLoad() {
    //    super.windowDidLoad()

        // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
   // }

    override init()
    {
        super.init()
        println(__FILE__, __FUNCTION__)
    }

    override init(window: NSWindow!)
    {
        super.init(window: window)
        println(__FILE__, __FUNCTION__)
    }

    required init?(coder: (NSCoder!))
    {
        super.init(coder: coder)
        println(__FILE__, __FUNCTION__)
    }

    override func windowDidLoad() {
        super.windowDidLoad()
        println(__FILE__, __FUNCTION__)
    }

}

Console output:

(…/MainWindowController.swift, init(coder:))
(…/MainWindowController.swift, windowDidLoad())
D.A.H
  • 858
  • 2
  • 9
  • 19