How to Load Nsview from Xib with Swift 3

Code to connect NSbutton to a method to load a NSView from Xib

Here you go. (Made it a second answer, rather than editing my previous one, because the previous one is already long enough. I'm still new here, and I hope this isn't against any rules. Not trying to game the reputation thing :)

The main view controller is still generated from the storyboard.

If this is somewhat verbose, I just wanted to show all the machinery. There are doubtless many other ways to do the same thing.


class ViewController: NSViewController {
// Initializing these objects here eliminates optionals
let stackView = NSStackView(views: [makeView(.purple), makeView(.black)])
let redButton = labeledButton("Red", action: #selector(showRed))
let greenButton = labeledButton("Green", action: #selector(showGreen))
let blueButton = labeledButton("Blue", action: #selector(showBlue))
// Action handlers are objective-c functions. @IBAction implies that
@objc func showRed(_ sender: Any) {
stackView.arrangedSubviews[1].layer?.backgroundColor = NSColor.red.cgColor
}
@objc func showGreen(_ sender: Any) {
stackView.arrangedSubviews[1].layer?.backgroundColor = NSColor.green.cgColor
}
@objc func showBlue(_ sender: Any) {
stackView.arrangedSubviews[1].layer?.backgroundColor = NSColor.blue.cgColor
}
// Helper to make multiple buttons. Static so it can initialize instance properties
class func labeledButton(_ stringValue: String = "", action: Selector) -> NSButton {
let button = NSButton()
button.action = action
button.translatesAutoresizingMaskIntoConstraints = false
button.bezelStyle = NSButton.BezelStyle.rounded
button.title = stringValue
return button
}
// Helper to make multiple views. Also must be static
class func makeView(_ color: NSColor) -> NSView {
let vw = NSView()
vw.translatesAutoresizingMaskIntoConstraints = false
vw.wantsLayer = true
vw.layer?.backgroundColor = color.cgColor
return vw
}

private func createStackView() {
// Configure & add the stack view
stackView.orientation = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
// Pin it to the enclosing view
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// Set widths of subviews, and pin them to the stack view
stackView.arrangedSubviews[0].widthAnchor.constraint(equalToConstant: 100).isActive = true
stackView.arrangedSubviews[1].widthAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
stackView.arrangedSubviews[0].topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
stackView.arrangedSubviews[1].topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
stackView.arrangedSubviews[0].bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
stackView.arrangedSubviews[1].bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
// Add the buttons to the left view
stackView.arrangedSubviews[0].addSubview(redButton)
stackView.arrangedSubviews[0].addSubview(greenButton)
stackView.arrangedSubviews[0].addSubview(blueButton)
// Space them vertically, and center them horizontally
redButton.centerXAnchor.constraint(equalTo: stackView.arrangedSubviews[0].centerXAnchor).isActive = true
greenButton.centerXAnchor.constraint(equalTo: stackView.arrangedSubviews[0].centerXAnchor).isActive = true
blueButton.centerXAnchor.constraint(equalTo: stackView.arrangedSubviews[0].centerXAnchor).isActive = true

redButton.topAnchor.constraint(equalTo: stackView.arrangedSubviews[0].topAnchor, constant: 40).isActive = true
greenButton.centerYAnchor.constraint(equalTo: stackView.arrangedSubviews[0].centerYAnchor).isActive = true
blueButton.bottomAnchor.constraint(equalTo: stackView.arrangedSubviews[0].bottomAnchor, constant: -40).isActive = true

}

override func viewDidLoad() {
super.viewDidLoad()
createStackView()
}

}

Swift 3 Load xib. NSBundle.mainBundle().loadNibNamed return Bool

First of all the method has not been changed in Swift 3.

loadNibNamed(_:owner:topLevelObjects:) has been introduced in macOS 10.8 and was present in all versions of Swift. However loadNibNamed(nibName:owner:options:) has been dropped in Swift 3.

The signature of the method is

func loadNibNamed(_ nibName: String, 
owner: Any?,
topLevelObjects: AutoreleasingUnsafeMutablePointer<NSArray>?) -> Bool

so you have to create an pointer to get the array of the views on return.

var topLevelObjects = NSArray()
if Bundle.main.loadNibNamed("CardView", owner: self, topLevelObjects: &topLevelObjects) {
let views = (topLevelObjects as Array).filter { $0 is NSView }
return views[0] as! NSView
}

Edit: I updated the answer to filter the NSView instance reliably.


In Swift 4 the syntax slightly changed and using first(where is more efficient:

var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(assistantNib, owner: self, topLevelObjects: &topLevelObjects) {
return topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}

Custom NSView with NSStackView

I have found the answer, you need to initiate properly the view. To do so :

class CustomView: NSView, NSTableViewDelegate, NSTableViewDataSource {

@IBOutlet var viewOutlet: NSView!
@IBOutlet weak var segmentControlOutlet: NSSegmentedControl!
@IBOutlet weak var tableViewOutlet: NSTableView!

override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
Bundle.main.loadNibNamed("CustomView", owner: self, topLevelObjects: nil)
addSubview(viewOutlet)
viewOutlet.frame = self.bounds
viewOutlet.autoresizingMask = [.height, .width]
}

required init?(coder: NSCoder) {
super.init(coder: coder)
Bundle.main.loadNibNamed("CustomView", owner: self, topLevelObjects: nil)
addSubview(viewOutlet)
viewOutlet.frame = self.bounds
viewOutlet.autoresizingMask = [.height, .width]
}

override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)

// Drawing code here.
}
}

Please take car that when you load the nib name, "CustomView" is the name of your xib file ;)

How to use custom NSView in Interface Builder?

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.

Load multiple instances of a NSView from Nib

Use the filter function to get the NSView instance

func loadView() -> NSView {
var topLevelObjects = NSArray()
Bundle.main.loadNibNamed("CustomView", owner: self, topLevelObjects: &topLevelObjects)
let views = (topLevelObjects as Array).filter { $0 is NSView }
return views[0] as! NSView
}


Related Topics



Leave a reply



Submit