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:
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)
}
}
}
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
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.
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:
File's Owner
to your new class' name.required init?(coder: coder)
let newNib = NSNib(nibNamed: myName, bundle: Bundle(for: type(of: self)))
where myName is the name of the XIB.newNib.instantiate(withOwner: self, topLevelObjects: nil)
the new NSNibself
. Do this in a for loop over the constraints
property. Alternatively you can simply create the constraints as you know them to be.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.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.
Here's some code to go with Bills solution:
We can create a protocol LoadableNib
to have our custom views conform to the requirements and extend it to get this functionality if it does:
protocol LoadableNib {
var contentView: NSView! { get }
}
extension LoadableNib where Self: NSView {
func loadViewFromNib() {
let bundle = Bundle(for: type(of: self))
let nib = NSNib(nibNamed: .init(String(describing: type(of: self))), bundle: bundle)!
_ = nib.instantiate(withOwner: self, topLevelObjects: nil)
let contentConstraints = contentView.constraints
contentView.subviews.forEach({ addSubview($0) })
for constraint in contentConstraints {
let firstItem = (constraint.firstItem as? NSView == contentView) ? self : constraint.firstItem
let secondItem = (constraint.secondItem as? NSView == contentView) ? self : constraint.secondItem
addConstraint(NSLayoutConstraint(item: firstItem as Any, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: secondItem, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
}
}
}
Then in our custom view class:
class YourViewClass: NSView, LoadableNib {
@IBOutlet var contentView: NSView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadViewFromNib()
}
}
Don't forget the xib setup as per Bill's answer (class of File's Owner to your custom view class, and a container NSView
to hold the view contents and connect to the contentView
outlet.
Essential steps:
MyView.swift
looks like belowMyView.xib
MyView
contentView
IBOutlet-
class MyView: NSView {
@IBOutlet var contentView: NSView!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
setup()
}
private func setup() {
let bundle = Bundle(for: type(of: self))
let nib = NSNib(nibNamed: .init(String(describing: type(of: self))), bundle: bundle)!
nib.instantiate(withOwner: self, topLevelObjects: nil)
addSubview(contentView)
// Autolayout code using cartograhpy library
constrain(self, contentView) { view, subview in
subview.edges == view.edges
}
}
}
It is similar to the steps from Bill, but I think Bill's step 6 can be simplified. The contentView
can simply be added to the custom view, and then add a constraint to fill.
Also on my blog post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With