Search code examples
macosswift3xcode8macos-sierraappkit

How to use custom NSView in Interface Builder?


I have watched numerous tutorials on how to create a custom View in Interface Builder. All are for iOS but MacOS should be similar, no? I have tried a few methods but none are completely successful. The init(coder:) calls the NIB instantiation (either through Bundle.main.loadNibNamed or NSNib which, in turn, calls init(coder:) and ends up with infinite recursion if I class the main view in my nib as my custom class

If I use a standard class then make file's owner my custom class that works better but still isn't right.

Is there an example that creates a custom control, using AppKit, that works? The closest that I have come displays the custom control but none of the autolayout settings work.

It must be fairly simple but I haven't figured it out yet.

Here is what I have so far:

  1. A new class MyControl


import Cocoa

@IBDesignable class MyControl: NSView {

@IBOutlet var customView: NSView!  // The top level NSView
@IBOutlet weak var insideButton: NSButton!  // The button inside the view

let myName: String

required init?(coder: NSCoder) {
    super.init(coder: coder)

    if Bundle.main.loadNibNamed("MyControl", owner: self, topLevelObjects: nil) {
        addSubview(customView)
    }
}

}

  1. A nib based on NSView with contains a centered NSButton. The File's Owner class is set to MyControl, the top level view remains as NSView

  2. The Main.storyboard has a Custom View classed as MyControl centered with height and width set.

When I view Main.storyboard it has a frame for the custom view but it is blank.

When I run the application the window that displays is blank.


Solution

  • After much searching and a lot of help from Apple Support I have found that creating and using a custom control is very easy in AppKit. It's just that it is like a key in the lock, unless you get it right you won't get much at all.

    I have created a sample project and posted it to GitHub here: https://github.com/ctgreybeard/SwiftCustomControl

    It's a small project and I hope I have fully commented it so that someone else can understand it.

    The gist of the process is this:

    1. In Interface Builder create a XIB and a subclass of NSView. They should be the same name but this is not required.
    2. For the XIB change the class of File's Owner to your new class' name.
    3. Build your new custom control as you want it to be.
    4. Include an IBOutlet in your class referencing the top-level NSView in the XIB. Also include any other actions or outlets that your control needs.
    5. Create the initializer required init?(coder: coder)
    6. Within that initializer:
      1. Load the nib using let newNib = NSNib(nibNamed: myName, bundle: Bundle(for: type(of: self))) where myName is the name of the XIB.
      2. newNib.instantiate(withOwner: self, topLevelObjects: nil) the new NSNib
      3. Recreate all the existing constraints from the old top-level NSView replacing the old NSView with self. Do this in a for loop over the constraints property. Alternatively you can simply create the constraints as you know them to be.
      4. self.addSubview for all the old top-level subviews This is easily done in a for loop over the subviews array in the old NSView.
      5. Apply the new array of constraints you created above.

    You're done ... the custom control should now appear correctly in Interface Builder and the app.

    Commentary: This, as simple as it is, really shouldn't be necessary. I should be able to simply use my custom class name in the top-level NSView in the XIB and be done with it. The code in the init is simply replacing that top-level NSView with our custom view.