How to keep code and images in multiple bundles in Xcode5

Apple invented “bundles” and “frameworks” to greatly improve the ease of development and maintenance of OS X projects. They are really good.

But Apple modified Xcode to try and stop people using these on iOS. I don’t know why.

Today I delved into this to find out the correct, working, simplest/easiest solution…

What do we want? RE-USE! NO BUGS!

(and an Easy Life)

We want:

  • Re-usable code goes in one Target (Apple’s nomenclature)
  • Re-usable assets goes in one Target (ideally the same as above!)
  • App targets embed the above targets, and add whatever custom code + assets they need
  • Everything is updated and maintained automatically

The brief solution:

  1. Create a Static Library target and move all your code there
  2. Create a Bundle and move all your assets there
  3. …find that the above Doesn’t Work becuase of Xcode5 bugs…
  4. …scroll to the end of this page, where there’s a step-by-step bullet point list of fixes. Become happy again :)

Static Library bundles are broken in Xcode5

You’re a Good Developer, and you follow Apple’s design and guides. You put your code, images and other re-usable assets into a Static Library Bundle.

Everything fails. It turns out that someone made Xcode5 corrupt iOS apps (iOS only! OS X apps are left alone) if they contain Bundles. There are multiple bugs outstanding against Xcode 4 and Xcode 5 here; the worst of them “accidentally” caused Xcode4 to stop submitting apps to the App Store … but thankfully that appears mostly fixed now (I didn’t get it with Xcode5).

It’s important to note that we are not hacking Xcode here; everything we do is correct and legal, we’re just working around bugs in Xcode’s GUI, or its default settings, or its build commands, etc.

As far as I can guess … it’s all an accidental side-effect of disabling Frameworks in iOS. (Which, of course, is another head-scratcher. iOS developers have already proven there is no reason to not have Frameworks, and in general Apple wants you to have Frameworks. But for some reason – maybe a good one, we don’t know because Apple hasn’t divulged it – the Xcode team broke this stuff years ago and hasn’t got around to fixing it.

Some googling reveals that you can workaround this by having TWO bundles:

  1. Bundle1: use the “Static Library” template in Xcode5. DO NOT INCLUDE ANY IMAGES (or text files, or … anything that “doesn’t get compiled as source-code”. Xcode5 will silently delete non-source-code items. Xcode5 will, by default, AUTOMATICALLY create a “copy” phase for the non-source files (e.g. images) and claim (lies!) that it will copy them. Then it will cackle madly at you while it deletes them.
  2. Bundle2: use the “OS X bundle” template in Xcode5. There is no such thing as an “OS X bundle”, but Apple’s XCode team wants to trick you into “not using them”. As a smart developer … you’re not fooled! Include all your NON-compiled assets here (images, text files, plist files, CoreData model files, etc)

…and then it still doesn’t work. After some more googling you discover a handful of bugs you have to workaround:

  1. BUG 1: Xcode has a major bug where “iOS apps must have ALL bundles set to “Skip Install = YES” or else Xcode5 will silently crash during build phase” (and it’ll prevent you uploading to the App Store)
  2. BUG 2: The error message to tell you that Xcode has crashed internally is missing, and so you get no notification – but the “distribute to App Store” / “distribute to AdHoc/Enterprise” buttons simply … vanish
  3. BUG 3: sometimes Xcode5 sets “Skip Install” to “No” for new targets, even though it shouldn’t, and even though this triggers BUG1 above
  4. BUG 4: by default, Xcode5 makes bundles “only work on OS X” (even though there’s no such concept – A bundle is simply “like a zip file: it’s a container for random files”, it has no notion of OS X / not OS X)

[NSBundle mainBundle] methods stop working

There is a “correct way” and an “incorrect way” for an operating system to load resources. All OS’s except Apple’s do it the “correct” way.

Basically … NSBundle is a very old library, and is missing a core feature that is needed by every non-trivial project: recursive search. You can add it:

Fix for loadNibNamed:

+(NSArray *) loadNibFromAnyBundleWithName:(NSString*) nibName owner:(id)owner options:(NSDictionary *)options
{
	if([[NSBundle mainBundle] pathForResource:nibName ofType:@"nib"] != nil)
	{
		return [[NSBundle mainBundle] loadNibNamed:nibName owner:owner options:options];
	}
	else
	{
		NSArray* allAppBundles = [NSBundle allBundles];
		
		for(  NSBundle* appBundle in allAppBundles )
		{
			if([appBundle pathForResource:nibName ofType:@"nib"] != nil)
			{
				return [appBundle loadNibNamed:nibName owner:owner options:options];
			}
			else
			{
				/** This only recurses one level deep; it is exceptionally rare to need to go multi-levels,
				 and really APPLE NEEDS TO FIX THEIR RESOURCE METHODS */
				for( NSURL* subURL in [appBundle URLsForResourcesWithExtension:@"bundle" subdirectory:nil])
				{
					NSBundle* subBundle = [NSBundle bundleWithURL:subURL];
					
					if([subBundle pathForResource:nibName ofType:@"nib"] != nil)
					{
						return [subBundle loadNibNamed:nibName owner:owner options:options];
					}
				}
			}
		}
		
		return nil;
	}
}

Fix for CoreData

-(NSManagedObjectModel*) dataModel
{
	if( _mom == nil )
	{
		NSString* momdPath = [[NSBundle mainBundle]pathForResource:self.modelName ofType:@"momd"];
		NSURL* momdURL = nil;
		
		/**
		 WHEREVER your MOMD file is hiding, we'll find it!
		 */
		if( momdPath == nil )
		{
			NSArray* allAppBundles = [NSBundle allBundles];
			for(  NSBundle* appBundle in allAppBundles )
			{
				momdURL = [appBundle URLForResource:self.modelName withExtension:@"momd" subdirectory:nil];
				
				if( momdURL == nil ) // not found, so check the bundle for sub-bundles
				{
					for( NSURL* subURL in [appBundle URLsForResourcesWithExtension:@"bundle" subdirectory:nil])
					{
						NSBundle* subBundle = [NSBundle bundleWithURL:subURL];
						momdURL = [subBundle URLForResource:self.modelName withExtension:@"momd" subdirectory:nil];
						if( momdURL != nil )
						{
							break;
						}
					}
				}
				
				if( momdURL != nil )
				{
					break;
				}
			}
			
			NSAssert( momdURL != nil, @"Failed to find the momd file for incoming modelName = %@. Maybe you forgot to convert your MOM to a MOMD? (Xcode major bug: used to do this automatically, now it doesn't)", self.modelName );
		}
		else
			momdURL = [NSURL fileURLWithPath:momdPath];
		
		_mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:momdURL];
	}
	
	return _mom;
}

(taken from my “out-dated but still useful” set of CoreData fixes on GitHub)

Fix for …

You get the picture, I think.

You may be tempted…

…to fix Bug4 above by going to the Build Settings for bundle 2, find the “build for: OS X” dropdown, and change it to “build for: iOS”, and suddenly everything works!

But it doesn’t. As you discover when you try to load images.

…now you find: UIImage imageNamed: is broken

Then … imageNamed: stops working. WTF?

There’s two things going wrong here:

  1. imageNamed internally re-uses [NSBundle mainBundle], which we know is already crappy
  2. Xcode5 new to Xcode5, Xcode4 didn’t do this silently deletes all PNG files and replaces them with TIFF files

Problem 1: imageNamed ?

Googling suggests you’re screwed. “imageNamed” is not (only) a convenience method, it includes a powerful built-in image-cache that’s part of iOS, and which you cannot use any other way.

BUT … it turns out that some nice person at Apple actually did write (most of) the missing parts of imageNamed:, and simply forgot to tell us they’d done it. imageNamed: does support loading from any bundle it just doesn’t do it transparently

Instead of:

[UIImage imageNamed:@”image.png”];

you simply have to use:

[UIImage imageNamed:@”MyBundleName.bundle/image.png”];

Really! It’s almost magical…

Note that Xcode5’s bundle template moves all image files from their sub-folders into the root of the bundle. Just like the main bundle of your app (sort-of). If your images are in sub-folders … ignore the sub-folder. Just stick the image name on the end of the bundle name, as above

Problem 2: Apple deletes all PNG files

What. The. Firkin?

Here’s a StackOverflow answer that explains what’s happened.

But why now? Because Xcode5 has been “upgraded” so that it prevents you from changing that OSX specific setting. This is a great idea – it’s hiding settings that “don’t make sense” on iOS … except: this is another BUG in Xcode5: that setting DOES make sense on iOS.

So … switch Bundle2 back to compiling for “OSX”(instead of iOS), and the missing setting re-appears. Xcode kindly informs us that this setting is currently set to something other than the default, correct value (it’s bolded), so you can either revert it to default, or simply set it to “FALSE”/”NO” as per the StackOverflow answer.

…and then flip it back to building for iOS again, or else Xcode5 will remove the bundle when you make your app.

TL;DR … gimme easy steps to fix it!

Finally, we have enough info to solve the Apple bugs in one fell swoop with an easy, maintainable, robust setup:

  1. Create bundles 1 and 2 above
  2. Fix bundle2, so that it stops converting all PNG to TIFF
  3. Set bundle2 to compile as iOS, or it will be silently removed from the app
  4. Set bundle2 to compile “all” architectures, or it will work on sim but not device, or vice versa
  5. Open the “Products” twisty in your left hand bar, and find the named output file for Bundle2. Copy/paste the name
  6. …due to another bug in Xcode5, you can’t copy/paste that name. You have to:
    1. Build the bundle once (works around a THIRD bug in Xcode5) so the name becomes Black rather than Red
    2. right-click the product, “Open in Finder”, and copy/paste the file-name from Finder.
  7. Change your imageNamed from:
    • [UIImage imageNamed:@”image.png”];
  8. …to:
    • [UIImage imageNamed:@”[name of bundle2].bundle/image.png”];

Et voila! Simple, maintainable, reusable code that works perfectly with Xcode4 and Xcode5! Static libraries, themeable apps, oh MY!

3 thoughts on “How to keep code and images in multiple bundles in Xcode5

  1. Dominik

    Hi Adam,

    thanks for the great article! Could you please precise the step-by-step part? Especially the following:
    With the template of bundle2 (osx) I have created the .bundle file with all my images. Then I have tried to add this .bundle file to my static library project by trying to add the bundle to my frameworks. This ends up in a “Dependency Analysis Error” telling me that .bundle has a “unexpected file type ‘wrapper.plug-in’ in Framework & Libraries build phase”.
    Do I either have to include the .bundle to my resources? But then the staticLib.a file did not include the .bundle but the bundle file will be shipped separately! At the end I will have 3 files:
    1. .h
    2. .bundle
    3. staticLib.a

    Is this correct? No, I don’t think so. I thought, that at the end I will be able to ship 2 files:
    1. .h
    2. staticLib.a (incl. bundle)

    Thanks for your time to explain more in depth what’s wrong here.

    bye
    dominik

  2. adam Post author

    You were correct first time. The hoops that Apple makes us jump through are:

    1. Static libraries CAN ONLY CONTAIN BINARY COMPILED SOURCE FILES

    2. Header files MUST BE SHIPPED SEPARATELY

    3. Assets MUST BE SHIPPED SEPARATELY

    So you do indeed end up with:

    1. A folder full of header files
    2. a lib***.a static library file
    3. A ****.bundle assets-bundle

    …however, Xcode has various features for semi-automatically finding/packaging the Header files. There be Dragons! Have a look at the widely-used script for bundling iOS static libraries (That builds both Device and Simualtor, and puts them into a single lib***.a file) to see *one* way of achieving this: http://stackoverflow.com/questions/3520977/build-fat-static-library-device-simulator-using-xcode-and-sdk-4

  3. adam Post author

    If you want to ship fewer files, your only choice is to create a “fake framework” (which works perfectly, but Apple refuses to let you do inside Xcode, for some reason they won’t tell us).

    Some big companies (e.g. Parse/Facebook) have bitten the bullet and now use fake frameworks – it’s the Right Way, but it’s a lot of hassle and irritation to configure by hand.

Leave a Reply

Your email address will not be published. Required fields are marked *