Seiten

Tuesday, April 14, 2015

After the recent update of Xcode to version 6.3 iOS developers reported to encounter exceptions of kind

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: Could not load NIB in bundle: ...

followed by the path to the running iPhone or iPad application, and the name of the NIB file the it was trying to load. These exceptions appear to fire at runtime only, not at compile time. So make sure you test your iOS applications completely after building them with Xcode 6.3!

The reason for this issue seems to be a bug of Xcode 6.3 when compiling XIB files using size classes. Size classes is a concept introduced with iOS 8 which enables you to define view layouts for different UI orientations and different iPhone and iPad devices within a single Interface Builder document. A very good introduction to size classes is given at adaptioncurve.net.

So let's have a look at a little example to elaborate the problem!

We have an Xcode project with the deployment target set to iOS 7 and Universal device support:

We have two XIB files: TestSizeClasses.xib is targeted to iPhone and iPad. TestSizeClassesIphone~iphone.xib is named according to Apple's conventions to be targeted to iPhone only:

Both XIB files have size classes enabled:

After compiling these files with XCode 6.2, and running them in Simulator let's have a look at the NIB files created at compile time. You will find these NIBs in your user directory under a path of this structure:

TestSizeClasses.xib was split into two device specific NIB files, but TestSizeClassesIphone~iphone.xib was compiled to a single, iPhone specific NIB file because the '~' - part of the file name signals to the compiler that we care about device specific layouts by ourselfs. So everything is fine!

Now lets compile the same files with Xcode 6.3 and have a look at the results:

As you can see, Xcode appends another '~iphone' part to the name of TestSizeClassesIphone~iphone.nib, although we wanted it to leave our file name alone! So this is the bug in Xcode 6.3 XIB processing which will lead to runtime 'could not load nib' exceptions when your app tries to load the badly named nib file.

The bug will haunt you when you have size classes enabled for a XIB file, and the deployment target of your Xcode project is set to something earlier than iOS 8. Because size classes are only supported in iOS 8.0 and later, Xcode will compile the old device specific nib files automatically for older iOS versions. It doesn't matter whether your app is universal or targeted to iPad or iPhone only. As soon as you name a XIB device specific using the '~', your app will crash.

Now that we know the reasons for this exception, we have four options to handle it:

Option 1: Target to iOS 8.0 or later

The simplest option would be to target your app to iOS 8 or later, thus the device specific NIB files would just not be created. But if you want to support older iOS versions, this is not an option for you.

Option 2: Use storyboards instead of XIB files

If you can afford to make the effort, migrate all your XIBs to storyboards. These will be compiled correctly by Xcode 6.3.

Option 3: Disable Size Classes

When you disable size classes for a XIB file, Xcode will force you to target this file to a specific device and will remove all data it thinks will not be of use to represent this device:

So choose this option only when you absolutely trust in that Xcode will not remove anything you need from your XIB files. As a rule of thump: If all layout constraints in a XIB apply to all size classes, you are safe. If different constraints apply to different size classes, think twice!

Option 4: Handle device specific XIBs in code

This is the less destructive of all options and it is not as expensive to implement as it might seem! All you have to do is to name your XIB device specific by avoiding Apple's naming conventions (the '~'), and add very few lines of code to load the correct NIBs.

So we rename our TestSizeClassesIphone~iphone.xib to TestSizeClasses-Iphone.xib. We create another TestSizeClasses-Ipad.xib to cover all devices. And we create a single TestSizeClassesViewController, which we set the file's owner of both XIB files:

After compiling the code, we'll find the following NIBs:

As you can see, Xcode created both, an ~iphone version and an ~ipad version of all of our NIB files. Now all we have to do is to tweak our Objective-C code to load the correct NIB version per device.

So open TestSizeClassesViewController and implement its init method to load device specific nib files:

- (id)init

{

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

{

return [selfinitWithNibName:@"TestSizeClasses-iPad"bundle:nil];

}

else

{

return [selfinitWithNibName:@"TestSizeClasses-iPhone"bundle:nil];

}

}

Do the same in all places of your code where the view controller is instantiated directly using initWithNibName:

...

TestSizeClassViewController* sizeClassController;

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

{

sizeClassController = [selfinitWithNibName:

@"TestSizeClasses-iPad"bundle:nil];

}

else

{

sizeClassController = [selfinitWithNibName:

@"TestSizeClasses-iPhone"bundle:nil];

}

...

This way, the app will load the TestSizeClasses-Iphone~iphone.nib and TestSizeClasses-Ipad~ipad.nib only. It will ignore the TestSizeClasses-Iphone~ipad.nib and TestSizeClasses-Ipad~iphone.nib.

That's all there is about this Xcode issue. Now it's up to you to decide which of the four options is the best for your needs.