Overriding Class Initializer

I am trying to fully understand some Cocoa classes, specifically NSDocumentController and NSDocument. To help me with this, I simply wanted to subclass NSDocumentController and NSDocument, overriding any interesting methods with a method that would simply print some information to console and call the super's method.

Simple, right? Not so much.

I run into this problem with convenience initializers. For example, I cannot

class Document: NSDocument {
    override init() {
        print("init")
        super.init()
    }

    // The compiler warns, "The initializer does not override a designated initializer from its superclass
    override convenience init(contentsOf url: URL, ofType typeName: String) throws {
        print("init(contentsOf:ofType)")
        super.init(contentsOf: url, ofType: typeName)
    }
}

I have figured out a workaround. However, it meant determining how Cocoa actually implements the the convenience initializer. Before moving on, I wanted to know if someone has determined how to do this without having intimate knowledge of the class being subclassed.

This is a relatively simple use case, used to spy on the message flow between NSDocumentController and NSDocument. However, there are more sophisticated use cases that might build on the definition of super's convenience initializers and it doesn't look possible. Hope someone can enlighten me concerning this subject matter.

Cheers,
-Patrick

The debugger can do what you want, without needing to subclass. For example:

  1. click the Add (+) button in the Breakpoint navigator;

  2. choose Symbolic Breakpoint from the pop-up menu;

  3. enter the symbol -[NSDocument initWithContentsOfURL:ofType:error:] using the full Objective-C name;

  4. change the action to Log Message with %B %H for the breakpoint name and hit count;

  5. enable the Automatically continue after evaluating actions option.

This satisfies my immediate use case. However, what if I want to expand on the functional capabilities of a super's convenience initializer? Not hard if I have source, but substantially more difficult (if at all possible) if I don't have the source.

The reason why you get this error is that your convenience initialiser is trying to call super. You need to call ‘across’ to the designated initialiser. I’ve found the diagram in the Initializer Delegation for Class Types section of The Swift Programming Language to be super helpful in discussions like this.

I have figured out a workaround. However, it meant determining how
Cocoa actually implements the the convenience initializer. Before
moving on, I wanted to know if someone has determined how to do this
without having intimate knowledge of the class being subclassed.

This is where I lose you. I think your main complaint here is that the information in the url and typeName parameters is getting lost when you initialise the correct way. That problem is caused by a poor interaction between Swift’s extremely rigorous initialisation rules and Cocoa’s much less rigorous implementation of those rules based on year’s of evolution. Internally NSDocument resolves this issue by calling the designated initialiser and then, on returning, applying this information (setting self.fileURL, calling read(from:ofType:), and so on). I presume that’s what you’re trying to avoid?

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Hi Eskimo,

I understand the sample I provided above doesn't work and breaks policy governing initialization. I simply wanted to see if someone had a workaround for the inability to override a super's convenience initializer.

Being a long time C++ programmer, I realize the importance of the policies put into place around class initialization in Swift. However, I do think they overstep bounds of what is practical.

The example I provided in the original post provides a great example. What if I want to sub-class NSDocument? Furthermore, I want an initializer with the same signature as the super, but this initializer needs to call the super's convenience initializer, plus do some additional work.

To make a long story short, you can't do this, because your new initializer must either called a super's designated initializer or one of self's initializers. If I know everything the super's convenience function does, then this isn't an issue, just an inconvenience (ironic, isn't it). In this case, I can simply call the super's designated initializer, write the code to emulate the super's convenience initializer, and finally add my own code. However, what if I don't have any familiarity with the implementation of the super's convenience function? I'm stuck in the mud.

Convenience initializers are starting to feel not so convenient. In C++, I could simply call a super's initializer without constraint. Is this always safe? No. However, it also doesn't present a unnecessary barrier to building a sub-class.

I think the underlying question here has to do with “Rule 1” of Swift’s class initialization story.

The document does not make clear why rule 1 prohibits a designated initializer from calling a convenience initializer of its superclass. That would still be “delegating up”, and at the superclass level it would still funnel across to a designated initializer before continuing up the hierarchy.

That's not how convenience initializers work. They call a designated initializer via dynamic dispatch, i.e. getting the most-derived version. This is an Objective-C pattern that Swift brought over for compatibility.


NSDocument's design is actually one of the cases that Swift's initializer model does not handle. Allowing convenience initializers to call super to another convenience initializer would be one way of doing so. (Nothing stops this from turning into infinite recursion, but the compiler in general can't stop you from infinite recursion, and you'd probably notice when your program crashed.)

1 Like

As I said, the Swift Programming Language section “Class Inheritance and Initialization” does not explain that.

It's in the "Designated and Convenience Initializers in Action" section further down.

Could you please quote the relevant passage?

a. The subsection you mention falls within the section I linked to, so I was already referring to it as well (indeed I quoted what I found relevant from that subsection above).

b. Searching the entire “Initialization” page for either “dynamic” or “dispatch” shows 0 occurrences.

c. The rules listed in the subsection you mention, which I also quoted above, specifically say “A convenience initializer must call another initializer from the same class.” This sounds to me very much like a description of static dispatch.

d. Similarly, the summary of those rules says “Convenience initializers must always delegate across.”

Thus, to my reading, the document does not explain that convenience initializers use dynamic dispatch when chaining to another initializer, and in fact it rather strongly (and repeatedly!) implies the opposite.

1 Like

In this example, the superclass for RecipeIngredient is Food, which has a single convenience initializer called init(). This initializer is therefore inherited by RecipeIngredient. The inherited version of init() functions in exactly the same way as the Food version, except that it delegates to the RecipeIngredient version of init(name: String) rather than the Food version.

I agree that it can be called out more explicitly. cc @krilnon