9

I'm looking to be able to pull out an instance of a UIView subclass from a Nib.

I'd like to be able to call MyCustomView.instantiateFromNib() and have an instance of MyCustomView. I'm almost ready to just port the working Objective-C code I have via the bridging header, but figured I'd try the idiomatic approach first. That was two hours ago.

extension UIView {
    class func instantiateFromNib() -> Self? {

        let topLevelObjects = NSBundle.mainBundle().loadNibNamed("CustomViews", owner: nil, options: nil)

        for topLevelObject in topLevelObjects {
            if (topLevelObject is self) {
                return topLevelObject
            }
        }

        return nil
    }
}

Now if (topLevelObject is self) { is wrong because "Expected type after 'is'". What I've tried after that shows a lot about what I don't understand about the Swift type system.

  • if (topLevelObject is Self) {
  • if (topLevelObject is self.dynamicType) {
  • if (topLevelObject is self.self) {
  • A million other variations that are not even wrong.

Any insight is appreciated.

rob5408
  • 2,972
  • 2
  • 40
  • 53

2 Answers2

18

Using the approach from How can I create instances of managed object subclasses in a NSManagedObject Swift extension? you can define a generic helper method which infers the type of self from the calling context:

extension UIView {

    class func instantiateFromNib() -> Self? {
        return instantiateFromNibHelper()
    }

    private class func instantiateFromNibHelper<T>() -> T? {
        let topLevelObjects = NSBundle.mainBundle().loadNibNamed("CustomViews", owner: nil, options: nil)

        for topLevelObject in topLevelObjects {
            if let object = topLevelObject as? T {
                return object
            }
        }
        return nil
    }
}

This compiles and works as expected in my quick test. If MyCustomView is your UIView subclass then

if let customView = MyCustomView.instantiateFromNib() {
    // `customView` is a `MyCustomView`
    // ...
} else {
    // Not found in Nib file
}

gives you an instance of MyCustomView, and the type is inferred automatically.


Update for Swift 3:

extension UIView {

    class func instantiateFromNib() -> Self? {
        return instantiateFromNibHelper()
    }

    private class func instantiateFromNibHelper<T>() -> T? {
        if let topLevelObjects = Bundle.main.loadNibNamed("CustomViews", owner: nil, options: nil) {
            for topLevelObject in topLevelObjects {
                if let object = topLevelObject as? T {
                    return object
                }
            }
        }
        return nil
    }
}
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    Fantastic! I had dipped my toe in generic methods to try and solve this and failed miserably. Thanks! – rob5408 Jul 01 '15 at 17:21
  • I found a strange bug with this where it works on the Simulator and on device if done via development provision, but over the air it doesn't use the view's class specified within the Nib. I get crashes reporting that another view with another type is returned and I treat it as the expected type. Strange. – rob5408 Jul 22 '15 at 18:50
  • Switching the optimization level flag for release builds to None worked. Now I gotta figure out how to fix it correctly. – rob5408 Jul 22 '15 at 19:04
  • Thanks for this. I've copied this approach here http://stackoverflow.com/a/43397745/8047 and it works. For arrays, however: I cannot get it to work for `[Self]` in the outer method. Any hints? Error is `'Self' is only available in a protocol or as the result of a method in a class; did you mean 'NSManagedObject'?` – Dan Rosenstark Apr 13 '17 at 18:46
2

I believe the conditional expression you're looking for is topLevelObject.dynamicType == self

Combining this with unsafeBitCast (which, by Apple's own documentation, "Breaks the guarantees of Swift's type system"), we can forcefully downcast topLevelObject to self's type. This should be safe because we already made sure that topLevelObject is the same type as self

This is one way to get around the helper method using generics that Martin R described.

extension UIView {
    class func instantiateFromNib() -> Self? {

        let bundle = NSBundle.mainBundle().loadNibNamed("CustomViews", owner: nil, options: nil)

        for topLevelObject in topLevelObjects {
            if topLevelObject.dynamicType == self {
                return unsafeBitCast(topLevelObject, self)
            }
        }
        return nil
    }
}

Note that Apple also says in their documentation for unsafeBitCast:

There's almost always a better way to do anything.

So be careful!

jperl
  • 1,066
  • 7
  • 14