How to load NSView from Xib properly?
My code:
var topLevelArray: NSArray? = nil
let outputValue = AutoreleasingUnsafeMutablePointer<NSArray>(&topLevelArray)
if Bundle.main.loadNibNamed("RadioPlayerView", owner: nil, topLevelObjects: outputValue) {
let views = outputValue.pointee
return views.firstObject as! RadioPlayerView
}
topLevelArray = nil
return nil
The problem is "outputValue" is a auto-release pointer, and as soon as I return from the function, the program crash with ACCESS_BAD_ADDRESS
I made an protocol and extension to do this:
import Cocoa
protocol NibLoadable {
static var nibName: String? { get }
static func createFromNib(in bundle: Bundle) -> Self?
}
extension NibLoadable where Self: NSView {
static var nibName: String? {
return String(describing: Self.self)
}
static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? {
guard let nibName = nibName else { return nil }
var topLevelArray: NSArray? = nil
bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self }
return views.last as? Self
}
}
Usage:
final class MyView: NSView, NibLoadable {
// ...
}
// create your xib called MyView.xib
// ... somewhere else:
let myView: MyView? = MyView.createFromNib()
I solved this problem with a slightly different approach. Code in Swift 5.
If you want to create NSView
loaded from .xib
to e.g. addSubview and constraints from code, here is example:
public static func instantiateView<View: NSView>(for type: View.Type = View.self) -> View {
let bundle = Bundle(for: type)
let nibName = String(describing: type)
guard bundle.path(forResource: nibName, ofType: "nib") != nil else {
return View(frame: .zero)
}
var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(nibName), owner: nil, topLevelObjects: &topLevelArray)
guard let results = topLevelArray as? [Any],
let foundedView = results.last(where: {$0 is Self}),
let view = foundedView as? View else {
fatalError("NIB with name \"\(nibName)\" does not exist.")
}
return view
}
public func instantiateView() -> NSView {
guard subviews.isEmpty else {
return self
}
let loadedView = NSView.instantiateView(for: type(of: self))
loadedView.frame = frame
loadedView.autoresizingMask = autoresizingMask
loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints
loadedView.addConstraints(constraints.compactMap { ctr -> NSLayoutConstraint? in
guard let srcFirstItem = ctr.firstItem as? NSView else {
return nil
}
let dstFirstItem = srcFirstItem == self ? loadedView : srcFirstItem
let srcSecondItem = ctr.secondItem as? NSView
let dstSecondItem = srcSecondItem == self ? loadedView : srcSecondItem
return NSLayoutConstraint(item: dstFirstItem,
attribute: ctr.firstAttribute,
relatedBy: ctr.relation,
toItem: dstSecondItem,
attribute: ctr.secondAttribute,
multiplier: ctr.multiplier,
constant: ctr.constant)
})
return loadedView
}
If there is no .xib file with the same name as the class name, then code will create class from code only. Very good solution (IMO) if someone wants to create the view from code and xib files in the same way, and keeps your code organized.
.xib
file name and class name must have the same name:
In .xib
file you should only have one view object, and this object has to have set class:
All you need to add in class code is instantiateView()
in awakeAfter
e.g.:
import Cocoa
internal class ExampleView: NSView {
internal override func awakeAfter(using coder: NSCoder) -> Any? {
return instantiateView() // You need to add this line to load view
}
internal override func awakeFromNib() {
super.awakeFromNib()
initialization()
}
}
extension ExampleView {
private func initialization() {
// Preapre view after view did load (all IBOutlets are connected)
}
}
To instantiate this view in e.g. ViewController you can create view like that:
let exampleView: ExampleView = .instantiateView()
or
let exampleView: ExampleView = ExampleView.instantiateView()
but Swift have problems sometimes with instantiate like that:
let exampleView = ExampleView.instantiateView()
in viewDidLoad()
in your controller you can add this view as subview:
internal override func viewDidLoad() {
super.viewDidLoad()
exampleView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(exampleView)
NSLayoutConstraint.activate(
[exampleView.topAnchor.constraint(equalTo: view.topAnchor),
exampleView.leftAnchor.constraint(equalTo: view.leftAnchor),
exampleView.rightAnchor.constraint(equalTo: view.rightAnchor),
exampleView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]
)
}