12

I created a test project in ios 8 beta 4 which as a main view controller and a second view controller created as a UIViewController subclass with a xib file.

I put a button on the main controller to present the second controller:

class ViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
}

@IBAction func testVCBtnTapped() {
    let vc = TestVC()
    presentViewController(vc, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

}

I run the app and pressing the button presents the second controller - all is well

Moving to xcode beta 5, I run the app and when I press the button the screen goes black.

Since I know they messed with the init code, I tried putting in overrides to see it that would fix it:

class TestVC: UIViewController {

override init() {
    super.init()
}
required init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
}

Same problem. Changing the required and overrides in to all possible combinations accepted by xcode has no effect.

If I use the storyboard to create another controller and segue to it all is well.

Any ideas?

EDIT - New info

  1. Tried nibName = nil in init - same problem
  2. Created the same app in objective c and it works fine

Apparently a swift beta 5 problem

Jim T
  • 2,480
  • 22
  • 21

7 Answers7

25

I can't tell whether it's a bug or not, but it is definitely a change. Of course they might change it back... In any case, the rule in seed 5 is:

The view controller will not automatically find its .xib just because it has the same name. You have to supply the name explicitly.

So in your case any attempt to initialize this view controller must ultimately call nibName:bundle: with an explicit nib name ("TestVC"), I assume.

If you want to be able to initialize by calling

let vc = TestVC()

as you are doing in your presenting view controller, then simply override init() in the presented view controller to call super.init(nibName:"TestVC", bundle:nil) and that's all you need (except that I presume you will also need the init(coder:) stopper discussed here).

EDIT You are absolutely right that this is a Swift-only problem. Well spotted. In Objective-C, initializing with init (or new) still works fine; the view controller finds its eponymous .xib file correctly.

ANOTHER EDIT The determining factor is not whether Objective-C or Swift calls init. It is whether the view controller itself is written in Objective-C or Swift.

FINAL EDIT The workaround is to declare your Swift view controller like this:

@objc(ViewController) ViewController : UIViewController { // ...

The name in parentheses gets rid of the name mangling which is causing the problem. It is likely that in the next release this will be fixed and you can take the @objc away again.

ANOTHER FINAL EDIT Bad news: the bug report I filed on this came back "works as intended". They point out that all I have to do is name the .xib file after the surrounding module, e.g. if my app is called NibFinder, then if I name my .xib file NibFinder.ViewController.xib, it will be found automatically when instantiating ViewController().

That is true enough, but in my view it merely restates the bug; the Swift lookup procedure is prepending the module name. So Apple is saying I should knuckle under and prepend the same module name to my .xib file, whereas I am saying that Apple should knuckle under and strip the module name off as it performs the search.

EDIT THAT IS TRULY TRULY FINAL This bug is fixed in iOS 9 beta 4 and all these workarounds become unnecessary.

Community
  • 1
  • 1
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • In any case please file a bug report. You've got a legitimate beef: they broke your code. On the other hand I have a bad feeling that this might be deliberate, exactly because, as you say, they are clearly tweaking initializers right now. – matt Aug 06 '14 at 05:07
  • 1
    Oh, I forgot to mention: a really weird thing is that if the view controller is called "MyViewController" and the .xib is called "MyView.xib" (dropping the "Controller" part), initializing with simple `init()` still works the way it used to. In other words, they killed this feature in one form but left it operative in another form. Is this because the main change is a bug or is it the bug the fact that they forgot to retract this as well? Only time will tell... – matt Aug 06 '14 at 05:08
  • I have tried that too. Explicitly calling the init with nibName "TestVC". still no go. My main app is totally broken since I use xib files every where. I created the exact same test with objc and it works fine. This seems to be swift only. – Jim T Aug 06 '14 at 05:40
  • I've got apps that use only xib files and they work fine, but only after I switched to explicit nib names. Post your project at GitHub and I'll fix it for you. – matt Aug 06 '14 at 13:35
  • matt - I tried your test app and it doesn't work for me. Are you using beta 5? Maybe something is wrong with my copy of xcode. I'll download a new copy and try again. – Jim T Aug 06 '14 at 18:12
  • sorry - misinterpreted what you were showing. You were showing the failure, not the solution. I will double check naming the xib file and upload an example if I can't get it to work. – Jim T Aug 06 '14 at 18:27
  • I show both the failure and the solution; the solution is in a comment after the bad line. So simply comment out the bad line and comment in the good line. – matt Aug 06 '14 at 18:38
  • matt, you're a God. I can't tell you how confused I was with this problem, and how far off I was with the initial assumption of the reason behind all this. Thanks a million! – Sergiu Todirascu Aug 26 '16 at 10:50
4

in case if You need support of iOS8. You can use extension on UIViewController

extension UIViewController {
    static func instanceWithDefaultNib() -> Self {
        let className = NSStringFromClass(self as! AnyClass).componentsSeparatedByString(".").last
        let bundle = NSBundle(forClass: self as! AnyClass)
        return self.init(nibName: className, bundle: bundle)
    }
}

and then, just create the instance this way

let vc = TestVC.instanceWithDefaultNib()
Alex
  • 1,603
  • 1
  • 16
  • 11
3

This trick works for me. If you have a base view controller class you can override the nibName property returning the class name (equals to the xib file name).

class BaseViewController: UIViewController {

   override var nibName: String? {
      get {
         guard #available(iOS 9, *) else {
            let nib = String(self.classForCoder)
            return nib
         }

         return super.nibName
       }
    }
}

All view controllers that inherit from this base class could load their xib. e.g. MyViewController: BaseViewController could load MyViewController.xib

Francesco Ceravolo
  • 1,019
  • 7
  • 5
  • Ingenious. You _could_ even implement the part where you lop off the "Controller" suffix. – matt Aug 24 '16 at 16:37
  • App will crash on iOS8, if you also have ViewControllers without associated xibs. To fix it, you need to check if nib file exits – KaterinaPetrova Jan 20 '17 at 13:55
2

I've had this problem recently, and my solution is to override the nibName method in baseviewController. My solution is as follows:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

init() {
    super.init(nibName: nil, bundle: Bundle.main)
}

override var nibName: String? {
    get {
        if #available(iOS 9, *) {
            return super.nibName
        } else {
            let classString = String(describing: type(of: self))
            guard Bundle.main.path(forResource: classString, ofType: "nib") != nil else {
                return nil
            }
            return classString
        }
    }
}
yinpan
  • 21
  • 1
  • 3
0

Here's code, based on Francesco answer. It contains check if nib file exits, so app will not crashed when you load UIViewController which doesn't have associated xib (you can reproduce it on iOS8)

override var nibName: String? {
    get {
        let classString = String(describing: type(of: self))
        guard nibBundle?.path(forResource: classString, ofType: "nib") != nil else {
            return nil
        }
        return classString
    }
}
override var nibBundle: Bundle? {
    get {
        return Bundle.main
    }
}
KaterinaPetrova
  • 462
  • 4
  • 12
0

Swift3:

extension UIViewController {
    static func instanceWithDefaultNib() -> Self {
        let className = NSStringFromClass(self).components(separatedBy: ".").last
        return self.init(nibName: className, bundle: nil)
    }
}
Bugs
  • 4,491
  • 9
  • 32
  • 41
0

I had a very similar problem on Xcode 10.1 and iOS 12.

This code worked:

class ExampleViewController: UIViewController {

    init() {
        super.init(nibName: "ExampleViewController", bundle: nil)
    }

}

But passing nil as nibName made it not load the XIB. When specifying the class name explicitly via @objc("ExampleViewController") this fixed it.

The problem was caused by the module name starting with the letters 'UI' (the project was called UIKitExample). When I renamed the target to something else, this fixed the problem.

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43