I’ve been dealing with this issue for years. I want a Swift custom view class based on a .xib file. That means the .swift file contains the logic for the class, but the definition of the contents, such as text fields, icons, constraints, etc., is in a .xib file. That way, I can visually edit the custom view, drop it into a parent in Interface Builder, and have it work properly. The trick is to have a base class for all “nib loadable” view classes so the nib loading code is hidden away once it’s working. I think I finally accomplished that to my satisfaction.
My first attempt at this required a protocol and then an extension for that protocol that would load the nib/xib file contents (I’ll just call it a nib for now, even though the files have a .xib extension). That code would also set some constraints because the nib contents are loaded into the custom view as a child and its children. The protocol worked okay, but some code was required to show up in every one of the classes that used the protocol. The new way to do this uses a base class instead of a protocol.
import Foundation
import UIKit
public protocol NibLoadable {
var nibName: String { get }
func commonInit()
}
public class NibLoadableView: UIView, NibLoadable {
public override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
public func commonInit() {
setupFromNib()
}
}
public extension NibLoadable where Self: UIView {
var nibName: String {
let typeName = String( describing: type( of: self ) ) // never use capitalized "Self" or only this base class info will be used.
let parts = typeName.split( separator: "." )
return String( parts[0] )
}
var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: nibName, bundle: bundle)
}
func setupFromNib() {
guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
view.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
view.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
}
The code above has a protocol, a class, and an extension. This is mostly because I already had a protocol and an extension and I just added the class and moved some of the protocol features, as well as other code, into it.
A class derived from this is expected to have an associated nib file whose base file name matches the class name. When the init function is called, no matter which variation it might be, the setupFromNib() function gets called. That function gets the class name and then loads the nib file. The root view of the nib file is added as a subview of the current custom view class, and constraints are added to make it fill the view.
In the previous incarnation of this code, the nibName variable was static, and all the various init functions had to be in the derived class. That was because the code also used Self.self to get the class name. Any reference to Self (capitalized) in the base class will return the name of the base class. This new code doesn’t use static variables, and type( of: self ) returns the actual class name of the class running the code.
You will notice that the class name is split into parts with the split() function. This is a leftover from a few failed experiments where the class name would get returned as something like “if.RingProgressView.Type”. I don’t recall when that happened, but the current code doesn’t have the same formatting as the class name. I just didn’t clean up all of the code.
Feel free to copy and use this code as the basis for your own code in your own projects. Most of it was pieced together by information on the internet, so I claim no ownership of it. I am just happy to be able to derive a class from this and have it work even when the derived class has zero code in it!