Some time ago I had to convert an Objective-C category I use to load UIView’s from XIB’s.
Here is the category:
@implementation UIView (NIB)
+ (UIView *)loadInstanceFromNib
{
NSArray* topLevelObjects = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner: nil options: nil];
for (id object in topLevelObjects)
{
if ([object isKindOfClass:[self class]])
{
return object;
break;
}
}
return nil;
}
This category lets you load a custom view from a XIB file with the same name as the custom view’s class. It does this by loading the XIB, looping through all the high-level objects in the XIB and return the first object with the same class as the loadInstanceFromNib
class method is called from.
CustomView *view = [CustomView loadInstanceFromNib];
How would we convert this category to Swift? Easy.
The category will become an extension on UIView in Swift.
With loading the XIB we encounter the first problem: NSStringFromClass(self)
in Swift will return us the class name prefixed by the module. In most cases this is the application name (e.g. MyApplication.CustomView
)
The XIB is called CustomView.xib. We need to strip the module name from the string returned by NSStringFromClass()
:
var className = NSStringFromClass(self)
className = split(className, { $0 == "."})[1]
Looping through the top level objects is the same in Swift.
Swift has special syntax to test if an object is of a certain type.
The is operator lets you test if an object is of a certain class. "Hello" is String
renders as true
The as? operator used in an if let construct will type cast the object to a type if the type matches.
// object is of type Any
if let text = object as? String {
// we only get here if object is a String. text is of type String
}
We can use the as? operator to test if the top level object is of our desired type.
for object in topLevelObjects {
if let view = object as? self {
return view
}
}
Unfortunately, we cannot use self as the return type. Swift wants to known the type at compile time. Objective-C didn’t care about that. In Objective-C the loadInstanceFromNib
method returns an UIView *
in Swift we want to return the right type.
We want this extension to be useable for any UIView
subclass. We can use Swift Generics to accomplish that:
extension UIView {
class func loadInstanceFromNib<T: UIView>() -> T? {
var className = T.description()
className = split(className, { $0 == "."})[1]
let nib = UINib(nibName:className, bundle:nil)
let topLevelObjects = nib.instantiateWithOwner(self, options:nil)
for object in topLevelObjects {
if let view = object as? T {
return view
}
}
return nil
}
}
Instead of looping through the top level objects with a for-loop, a more functional approach could be taken. Using a filter on the top level objects array will give us only the objects that match our criteria. This time we can use the is operator.
extension UIView {
static func loadInstanceFromNib<T: UIView>() -> T? {
var className = T.description()
className = split(className, { $0 == "."})[1]
let nib = UINib(nibName:className, bundle:nil)
let topLevelObjects = nib.instantiateWithOwner(self, options:nil)
return topLevelObjects.filter{ $0 is T }.first as? T
}
}
The extension can be used in the following way:
if let view:CustomView = UIView.loadInstanceFromNib() {
// Do something with the view
}
The type annotation is needed to let the compiler determine the type of T.