Best practices for handling subviews in custom NSView implementations of init(coder:) and encode(with:)?

When creating a custom NSView subclass that does not or cannot take advantage of initializer inheritance (possibly because it has some non-optional stored properties), we also need to provide our own implementation of init(coder:). Are there any caveats we need to be aware of if our subclass contains its own subviews?

To start, it seems that we need to provide our own implementation of encode(with:). For example:

override func encode(with aCoder: NSCoder) {
    super.encode(with: aCoder)
    aCoder.encode(subView, forKey: "subview")
}

Should the superclass's implementation of this method be called before or after the subclass's, or does the order not matter?

I found out the hard way that the order does matter in init(coder:). For example, this will lead to a runtime crash when subview is an instance of NSTextField:

required init?(coder decoder: NSCoder) {
    guard let subview = decoder.decodeObject(forKey: "subview") else {
        return nil
    }
    self.subview = subview
    super.init(coder: decoder)
}

Apparently we need to decode the superclass before any of its subviews:

required init?(coder decoder: NSCoder) {
    super.init(coder: decoder)
    guard let subview = decoder.decodeObject(forKey: "subview") else {
        return nil
    }
    self.subview = subview
}

One consequence of this appears to be that we must declare our subview properties as optional values because any non-optional values would have to be set before the superclass's initializer is called, but the subviews can't be set until after the superclass has been decoded.

Does this sound about right, or is there anything else that I've missed?

(The intricacies of initialization and optional values made this feel like a relevant question to ask here, but my apologies if it is more related to Cocoa than it is to Swift…)

1 Like

You know, I just realized that I may be going about this the wrong way. Rather than trying to encode an entire NSTextField, perhaps I should just be encoding the properties that I want to preserve, like this:

override func encode(with coder: NSCoder) {
    // Still not sure when to call the superclass's implementation here...
    super.encode(with: coder)
    // Only encode the text field's string value
    coder.encode(self.textField.stringValue, forKey: "label")
}

required init?(coder decoder: NSCoder) {
    // Only decode the text field's string value
    guard let text = decoder.decodeObject(of: NSString.self, forKey: "label") else {
        return nil
    }
    // Recreate the text field using the decoded string value
    self.textField = NSTextField(labelWithString: text as String)
    // Call the superclass's initializer
    super.init(coder: decoder)
}

This has the advantage of letting us call the superclass's initializer after we've initialized our subviews, but it does involve a bit more work to reconstruct the subviews and keep track of the properties that we'd like to preserve. Is this considered best practice?

The only possible wrinkle I could think of is trying to preserve a subview's delegate or some other reference to another view/controller. For example, let's say that both our custom NSView and its subviews share the same delegate—how can we ensure that we safely preserve that relationship?

1 Like