Is it possible to create an IBOutlet in a non-view class?

391 Views Asked by At

I'm creating a custom presentation controller for dimming the background when a view controller is presented. The presentation controller adds a couple of subviews when the transition begins which works great.

However, I would like to setup the chrome (the presentation "frame") in Interface Builder because that way it's easier to layout. Thus, I created a XIB file for designing the chrome. It includes a semi-transparent background view and a ❌-button in the upper left corner to dismiss the presented view controller. For these subviews I need outlets and actions in my presentation controller (which is not a UIViewController subclass).

In order to achieve that I set the XIB's file's owner to my custom presentation controller, both in Interface Builder and in code when instantiating the view:

lazy var dimmingView = Bundle.main.loadNibNamed("PresentationChromeView", 
                                   owner: self, 
                                   options: nil)?.first 
                                   as! UIView

I then created the respective outlets and actions by CTRL+dragging to my presentation controller:

@IBOutlet var closeButton: UIButton!

@IBAction func closeButtonTapped(_ sender: Any) {
    presentingViewController.dismiss(animated: true, completion: nil)
}

However, at run-time the app crashes because UIKit cannot find the outlet keys and when removing the outlets the actions methods are not triggered. So in neither case is the connection established.

Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<_SwiftValue 0x600000458810> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key closeButton.'

The only reason I can think of why this doesn't work would be that it's not allowed to create outlets and actions with classes that don't inherit either from UIView or UIViewController.

Is that assumption correct?

Is there a way to create outlets and actions with non-view(-controller) classes?

3

There are 3 best solutions below

3
On

OK... the main problem is that the XIB / NIB file has to be instantiated, not just load the first item.

All these changes are in DimmingPresentationController.swift:

// don't load it here...
//lazy var dimmingView = Bundle.main.loadNibNamed("DimmedPresentationView", owner: self, options: nil)?.first as! UIView
var dimmingView: UIView!

then...

private func addAndSetupSubviews() {
    guard let presentedView = presentedView else {
        return
    }

    // Create a UINib object for a nib (in the main bundle)
    let nib = UINib(nibName: "DimmedPresentationView", bundle: nil)

    // Instante the objects in the UINib
    let topLevelObjects = nib.instantiate(withOwner: self, options: nil)

    // Use the top level objects directly...
    guard let v = topLevelObjects[0] as? UIView else {
        return
    }
    dimmingView = v

    // add the dimming view - to the presentedView - below any of the presentedView's subviews
    presentedView.insertSubview(dimmingView, at: 0)

    dimmingView.alpha = 0
    dimmingView.frame = presentingViewController.view.bounds

}

That should do it. I can add a branch to your GitHub repo if it doesn't work for you (pretty sure I didn't make any other changes).

0
On

The issue that caused the app to crash was that the dimmingView's type could not be inferred (which is strange because the compiler doesn't complain). Adding the explicit type annotation to the property's declaration fixed the crash. So I simply replaced this line

lazy var dimmingView = Bundle.main.loadNibNamed("PresentationChromeView", 
                                   owner: self, 
                                   options: nil)?.first 
                                   as! UIView

with that line:

lazy var dimmingView: UIView = Bundle.main.loadNibNamed("PresentationChromeView", 
                                           owner: self, 
                                           options: nil)?.first 
                                           as! UIView

The outlets and actions are now properly connected.

Why this type inference didn't work or why exactly this fixes the issue is still a mystery to me and I'm still open for explanations. ❓

8
On

You can create IBOutlets in any class inheriting from NSObject. The issue here seems to be that you didn't set your custom class in interface builder:

'[<NSObject 0x60800001a940> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key closeButton.'

While decoding your Nib, NSCoder attempts to set the closeButton property on an instance of NSObject, which of course doesn't have this property. You either didn't specify your custom class or made an invalid connection.