When shrink-wrapping your code for later reuse you inadvertenly will come into the situation that you have some resources (strings, XIBs, images et al) in your project that you also want to be reused. So what do you do?
If only we had frameworks on iOS … then we could bundle the resources together with the code in a framework. But Apple does not want us to compile frameworks in Xcode since these could potentially contain code downloaded after the app review process.
Popular projects like ShareKit or the Facebook iOS SDK have approached this dilemma by simply putting all resources into a folder, giving it the “.bundle” extension and instruct users of their SDK to also add this bundle to the “Copy Bundle Resources” step of their respective apps.
In this here blog post I will show you a smarter way.
There are several problems with having resources contained in a bundle and having this bundle be a member of any projects.
Xcode does not directly “see” the contents of the bundle, instead the pbxproj only contains a reference to the bundle folder. This causes trouble for apps like our Linguan that parses the project file to find strings (and soon XIB) files. It simply cannot see them.
Here’s how it looks in the ShareKit project. If you ever see a x.bundle in a project you open, armed with the knowledge in this article, your reaction should be “that’s bad”.
Of course the maintainers of these projects “have their reasons”. But I hope that by reading this you will agree with me that the reasons to not do it like this are better.
Another issue is that this approach effectively disables the build time optimizations that Xcode carries out on the resources.
- strings files get converted into binary property lists
- XIB files get compiled into binary NIBs
- images get pngcrush-ed
- and many other actions for which there are build rules set up
That means resources that are simply bundled (by means of copying them together) are slower and not optimized for the mobile devices. You probably wouldn’t notice that for only a hand full of items, but if you have a large number of resources then these delays will add up slowing down your app. And even if that does not bother you very much then the engineer inside of you should cringe. It just feels so dirty…
Another reason for why it is a bad idea to hide files from Xcode is that it simply won’t know to rebuild your app if you make a change in one of the files hidden in such a bundle.
The Xcode build system has what is called dependencies. Implicit dependencies are source code files and resources that are part of certain targets. If such a dependence is modified then the incremental build process can determine which parts need to be re-compiled or re-optimized.
Say you change something in a single .m file. Xcode will not rebuild the entire app, but only create the .o for this updated file and then link it with the previously built (and unchanged .o files). Same with resources. Xcode only replaces resources in the output product .app bundle if it knows they where modified.
Resources in static bundles are invisible to Xcode and thus you always have to clean your build folder when doing a new build after changing them. Otherwise your updates will not propagate into your app.
Enter the Resource Bundle Target
Xcode, iOS and OS X have a mechanism to deal with folders that are looking like a single file but are actually containing multiple resources. This mechanism is modeled by the NSBundle class. You probably have worked with bundles before, namely the .app bundles that make up your app. Have you ever written [NSBundle mainBundle] before? I bet you did.
For NSBundle to be able to manage bundles it requires a special info.plist inside the bundle that contains some meta information, like an internal identifier. But once you have the bundle set up correctly you have multiple great options for getting at the files as I will show you below.
How to Set Up a Resource Bundle Product
A resource bundle is a product that we will set up a target for. It just so happens that my DTPinLockController project is in need of some love, so it will serve as our example for this tutorial.
As a first step I needed to move the files into the modern way of structuring my projects. That is, for component projects I have a Core and a Demo folder at the project root. Then each has a Source and a Resources sub-folder. DTPinLockController has XIBs, Images and Localizable.strings files.
The source code goes into a Static Library target. All resources go into a Resource Bundle target. I’ve omitted the setup for the library here as we want to focus our attention on the resources.
Next we need to set up the Resource Bundle target. The template for this can be found in the OS X section, under “Framework & Library”.
When removing the dynamic framework template Apple also removed the template for the Bundle. But with some minor modifications we can change the Mac bundle into one suitable for iOS. Since our bundle will not contain any executable code we don’t care about the settings for ARC and which foundation we want to use.
I like to name the bundle product that same as my component so that in the end I have a DTPinLockController.bundle containing my resources and a libDTPinLockController.a library to go with it.
The template creates 3 files in DTPinLockController:
- an empty InfoPlist.strings file, we don’t need that
- a DTPinLockController-Info.plist file, this is the META plist we need
- a DTPinLockController-Prefix.pch file. No code in bundle means we can also remove that.
I grab the plist and move it in to the root of my resources folder. The rest we can safely remove. I also rename it to Resources-Info.plist as to give an unsuspecting observer a hint that this is for the resource bundle.
We use this approach because the info plist for the resource bundle is too long to manually create. Note that there are several placeholders that get filled in during the build process, like the bundle name. All these settings come from the build settings which we are going to adjust next.
The default setting is to use the target name as name of products. I don’t like this because as I said about I want several products to be named DTPinLockController, but have the targets reflect their actual purpose.
Because of this I end up with 3 targets with descriptive names like “Demo App”, “Static Library” and “Resource Bundle”. These have different product names “Demo”, “DTPinLockController” and “DTPinLockController” respectively. The different extensions and the prefix “lib” are automatically added by Xcode.
There are all the modifications we need to do on the build settings for the resource bundle:
- CMD-Backspace on the Base SDK to have it be the same as for the entire project: Latest iOS
- Same on the “Mac OS X Deployment Target”
- Same on the Architectures
- Remove the reference to the PCH file
- Remove “Installation Directory”
- Set “Skip Install” to YES
- Adjust “Info.plist File” to the correct path, e.g. Core/Resources/Resources-Info.plist
The important part is the correct paths for the info plist and pch (none) and that Skip Install is YES because otherwise you get problems when trying to archive an app using the resource bundle. Since there is no code the compiler-related settings are really inconsequential, but I like to have them inherit from the project settings to have it look like a native iOS target.
In the build phases of the bundle target there is still a framework in “Link Binary with Libraries”. Remove this as well. It does not hurt as there is nothing to link it with, but might be confusing.
The final step for building the target is now to add the resources to it. You select the appropriate files in the project tree and set the checkmark in the right panel next to Resource Bundle.
Xcode has added a scheme for the new target with the original name. So I remove all schemes and auto generate them from scratch. This will create one scheme per target and name them the same as the targets.
Then we can try and see if the bundle builds correctly.
you can open up each build step and see that Xcode carried out the optimizations I alluded to above. In the case of this strings file you see –outputencoding binary that tells us that the strings file in the bundle will actually be binary.
You now have a DTPinLockController.bundle in the Products group that you can inspect to verify that this indeed is the case.
Using the Resource Bundle
There is a bug in Xcode 4.3.2 that might cause the list of resource to copy for the app target might get out of sync with the check marks in the project navigator. I had to manually go in the build phases of the demo app target and remove all the references to resources that are now part of the resource bundle.
Since the resource bundle is now a proper target we can add it as dependency to apps using it. This tells Xcode that if this dependency is somehow “dirty” then the app target is also in need of updating. So it will first build the dependent target and then include the product in the app build process.
This shows the Static Library set up as dependency and in “Link Binary With Libraries”. Now we also want the bundle product to be in the “Copy Bundle Resources” phase. Click the Plus button and add the bundle product.
This will make the bundle appear in the “Copy Bundle Resources”. If it wasn’t built before the app target then it will be built first, causing it to appear in the build products folder and from there it will be copied and included in the app product.
A Few Code Changes
If you reference images from a XIB and you put these images in the same resource bundle as the XIB then you don’t have to change anything. XIBs will load images from the same bundle that they were instantiated from.
However there are a couple of changes you need to make to your code so that the resources can be found at their new location.
The default macros for getting localized strings look for the strings files (aka “string tables”) in the main app bundle. These are the definitions of the 4 default macros and you can see that the bottom two have a way to specify the bundle to get the strings from whereas the first two hardcode the mainBundle.
#define NSLocalizedString(key, comment) \ [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil] #define NSLocalizedStringFromTable(key, tbl, comment) \ [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)] #define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)]
You can create your own custom macro or category or what-have-you to have a shortcut, but to promote understanding here’s the entire code that we need to first get an NSBundle instance from our resource bundle and then get one string from it.
Note that specifying a table of nil means that the strings file is called “Localizable.strings”, for a table name of “Name” the file is called “Name.strings”
// get the resource bundle NSString *resourceBundlePath = [[NSBundle mainBundle] pathForResource:@"DTPinLockController" ofType:@"bundle"]; NSBundle *resourceBundle = [NSBundle bundleWithPath:resourceBundlePath]; // get a string NSString *string = NSLocalizedStringFromTableInBundle(@"Set Passcode", @"DTPinLockController", resourceBundle, @"PinLock");
We probably want to have a category on NSBundle specific to our project to load and cache in a static variable the resource bundle. And to go with that a localized string macro that hard codes this resource bundle.
You probably have several places where you call [super initWithNibName:@"MyViewController" bundle:nil]. The nil in this case causes the NIB loader to assume that you mean the main bundle. Just as easily we can get the resource bundle and pass there here instead.
// get the resource bundle NSString *resourceBundlePath = [[NSBundle mainBundle] pathForResource:@"DTPinLockController" ofType:@"bundle"]; NSBundle *resourceBundle = [NSBundle bundleWithPath:resourceBundlePath]; // load View Controller from that UIViewController *vc = [[MyViewController alloc] initWithNibName:@"MyViewController" bundle:resourceBundle];
Do we see a pattern here? You betcha! We always first get the NSBundle instance for the resource bundle and then pass this as a parameter to some method that does something with the resource.
Ah, Graphics. With strings and XIBs both working the same way you would only be right to assume that there’s a imageNamed:inBundle: method, BUT… it is private. Radar rdar://10250430 by Cedric Luthi addresses this.
But fortunately for us we don’t really need this special method. The regular imageNamed can work with our resource bundles too!
All you need to do is prefix your image names with the name of the bundle, like this:
// load image from resource bundle UIImage *image = [UIImage imageNamed:@"DTPinLockController.bundle/Image.png"];
This works because besides being a bundle object the resource bundle is also folder that can be traversed via its path.
The cool thing about loading XIBs, strings and images from resource bundles like this is that you retain the awesome powers of these methods. Like pathForResource:ofType: will automatically deliver the correct language version for the device if the resource is localized. Or imageNamed: will still automatically load Retina graphics where applicable or device-specific images with ~ipad or ~iphone.
I told you before that you don’t need to do anything special if these images are referenced from XIBs contained in the same resource bundle. So this is only necessary for the cases where you load the images from code.
This simplified tutorial has all targets and resource consumers in the same project. But the same concepts also work if you add the component project as a sub-project.
I hope that I could convince you of the many advantages of resource bundles over static bundles. I’ve been successfully using them in almost all of my commercial components and many internal projects.
Bundle targets allow you to streamline the resource building process in a way that make larger projects way more effective and less error prone. Make it so!