1

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?

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • Sounds like you need to instantiate your "ChromeView" as a Child View Controller... then use delegate pattern to "pass the "X-button" tap up to the View Controller doing the actual presentation. – DonMag May 23 '17 at 12:20
  • That's the pattern I'll fall back to if I don't get this running. But if @floschliep is correct that _you can create IBOutlets in any class inheriting from NSObject_, then I simply wonder why this approach isn't working. – Mischa May 23 '17 at 12:28
  • Well, there is a difference between creating IBOutlets and instantiating the code that connects those outlets. I'm a little confused as to your structure? You have a UIView + UIButton in a XIB... that XIB's owner is *not* a UIView or UIViewController subclass? The thing is... if all you do is load a UIView (and subviews) from a XIB, but you don't load / instantiate the code associated with it, the IBOutlet and IBAction *don't exist*. – DonMag May 23 '17 at 13:00
  • Hmmm... I've re-read your question, and I'm a bit more confused now... You say you set the XIB file's owner to your "custom presentation controller" -- but ***that*** controller is not a `UIView` or `UIViewController` subclass? Can you put together a small example that shows just what you're doing (and throws the error)? – DonMag May 23 '17 at 13:33
  • Yes, that is exactly the question: Is it possible to set a class other than a `UIView` or a `UIViewController` subclass as a view's file owner (in order to create outlets and actions there)? – Mischa May 23 '17 at 15:01
  • I just posted a link to a sample project in a comment to [floschliep's answer](https://stackoverflow.com/a/44133185/2062785). – Mischa May 23 '17 at 15:02
  • Whoops? Your GitHub repo doesn't seem to have any code in it? – DonMag May 23 '17 at 15:32
  • Sorry, I didn't commit the changes. Now it does. – Mischa May 23 '17 at 15:34
  • OK - couple somewhat quirky things... but I have what may be a satisfactory solution for you... just checking a few things. – DonMag May 23 '17 at 16:06

3 Answers3

2

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.

floschliep
  • 513
  • 5
  • 14
  • I _did_ set the custom class for the XIB file's owner. (Otherwise I wouldn't have been able to drag the outlets into the file.) The outlets and actions are all connected with the file's owner. – Mischa May 23 '17 at 11:47
  • Sorry, I accidentally pasted the log output that was displayed when I passed `nil` for the owner parameter in the `loadNibNamed()` function. When I pass `self` there I get `[<_SwiftValue 0x600000458810> setValue:forUndefinedKey:] ...`. But your point is valid that the class for establishing the connections seems to be wrong. Updated my post accordingly. – Mischa May 23 '17 at 12:04
  • Can you reproduce this issue in a sample project? I just tried it and it worked perfectly for me. Maybe you find the issue while setting this up from scratch. – floschliep May 23 '17 at 12:46
  • Here's my sample project: https://github.com/mischa-hildebrand/PresentationControllerExperiment. Unfortunately, I ran into the same crash there. If you have a working project, would you mind sharing it in order to isolate the issue? – Mischa May 23 '17 at 14:53
  • The issue lies in this line: `lazy var dimmingView = Bundle.main.loadNibNamed("DimmedPresentationView", owner: self, options: nil)?.first as! UIView`. `self` refers to the class and not the object here. If you lazy load the variable this way, it will work: `lazy var dimmingView: UIView = { let bundle = Bundle.main.loadNibNamed("DimmedPresentationView", owner: self, options: nil)! return bundle.first as! UIView }()`. – floschliep May 23 '17 at 18:54
  • That sounds like a good explanation. But why is it then that `self` is suddenly interpreted as the _object_ when I only add the type annotation `lazy var dimmingView: UIView ...` without changing anything else? (See [my answer](https://stackoverflow.com/a/44140931/2062785)) – Mischa May 23 '17 at 22:11
  • My best guess is that this is a bug in the Swift compiler. Either both, declaring the type explicitly or having the compiler infer it, should work, or none of them. Initialization using a closure (my suggestion) will always work nonetheless. – floschliep May 24 '17 at 09:03
  • Agreed. I've changed my implementation to use a self-executing closure as it's probably the safest thing to do. – Mischa May 25 '17 at 12:54
0

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).

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Your code works great but you are in error with your explanation _why_. It doesn't matter if you call `instantiate()` on a `UINib` or use the `Bundle`'s `loadNibNamed()` function – it's just two different ways to do it (see: [Resource Programming Guide](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW24)). – Mischa May 23 '17 at 16:48
  • However, it pointed me in the right direction: Somehow (for a reason I still have to figure out) the `dimmingView`'s **type** `UIView` could not be inferred. Adding the explicit type annotation to the property declaration fixed the crash. All outlets and actions are now properly set. Thanks for helping me finding the issue here! – Mischa May 23 '17 at 16:49
  • 1
    Ah - you're absolutely right... It's been a while since I worked with xibs, and when I did I always loaded them as part of a class (plus it was Obj-C, not Swift). Glad you got it worked out. – DonMag May 23 '17 at 17:28
0

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. ❓

Mischa
  • 15,816
  • 8
  • 59
  • 117