转发:http://www.cocoanetics.com…

来源:互联网 发布:人工智能客服系统 编辑:程序博客网 时间:2024/04/29 18:48

Resource Bundles

May 01, 2012

When shrink-wrapping your code for later reuse you inadvertenlywill come into the situation that you have some resources (strings,XIBs, images et al) in your project that you also want to bereused. So what do you do?

If only we had frameworks on iOS … then we could bundle theresources together with the code in a framework. But Apple does notwant us to compile frameworks in Xcode since these couldpotentially contain code downloaded after the app reviewprocess.

Popular projects like ShareKit orthe FacebookiOS SDK have approached this dilemma by simplyputting all resources into a folder, giving it the “.bundle”extension and instruct users of their SDK to also add this bundleto the “Copy Bundle Resources” step of their respective apps.

In this here blog post I will show you a smarter way.

 

Binpress - The source-code marketplace

 

There are several problems with having resources contained in abundle and having this bundle be a member of any projects.

Not Visible

Xcode does not directly “see” the contents of the bundle, insteadthe pbxproj only contains a reference to the bundle folder. Thiscauses trouble for apps like our Linguan thatparses the project file to find strings (andsoon XIB) files. It simply cannot see them.

Here’s how it looks in the ShareKit project. If you ever see ax.bundle in a project you open, armed with the knowledge in thisarticle, 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 thereasons to not do it like this are better.

Not Optimized

Another issue is that this approach effectively disables the buildtime 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 copyingthem 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 willadd up slowing down your app. And even if that does not bother youvery much then the engineer inside of you should cringe. It justfeels so dirty…

Not Updating

Another reason for why it is a bad idea to hide files from Xcode isthat it simply won’t know to rebuild your app if you make a changein one of the files hidden in such a bundle.

The Xcode build system has what is called dependencies. Implicitdependencies are source code files and resources that are part ofcertain targets. If such a dependence is modified then theincremental build process can determine which parts need to bere-compiled or re-optimized.

Say you change something in a single .m file. Xcode will notrebuild the entire app, but only create the .o for this updatedfile and then link it with the previously built (and unchanged .ofiles). Same with resources. Xcode only replaces resources in theoutput product .app bundle if it knows they where modified.

Resources in static bundles are invisible to Xcode and thus youalways have to clean your build folder when doing a new build afterchanging them. Otherwise your updates will not propagate into yourapp.

Enter the Resource Bundle Target

Xcode, iOS and OS X have a mechanism to deal with folders that arelooking like a single file but are actually containing multipleresources. This mechanism is modeled by the NSBundle class. Youprobably have worked with bundles before, namely the .app bundlesthat 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 specialinfo.plist inside the bundle that contains some meta information,like an internal identifier. But once you have the bundle set upcorrectly you have multiple great options for getting at the filesas 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. Itjust so happens that my DTPinLockController projectis in need of some love, so it will serve as our example for thistutorial.

As a first step I needed to move the files into the modern way ofstructuring my projects. That is, for component projects I have aCore and a Demo folder at the project root. Then each has a Sourceand a Resources sub-folder. DTPinLockController has XIBs, Imagesand Localizable.strings files.

The source code goes into a Static Library target. All resources gointo a Resource Bundle target. I’ve omitted the setup for thelibrary here as we want to focus our attention on theresources.

Next we need to set up the Resource Bundle target. The template forthis can be found in the OS X section, under “Framework &Library”.

When removing the dynamic framework template Apple also removed thetemplate for the Bundle. But with some minor modifications we canchange the Mac bundle into one suitable for iOS. Since our bundlewill not contain any executable code we don’t care about thesettings for ARC and which foundation we want to use.

I like to name the bundle product that same as my component so thatin the end I have a DTPinLockController.bundle containing myresources 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 weneed
  • a DTPinLockController-Prefix.pch file. No code in bundle means wecan 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 toResources-Info.plist as to give an unsuspecting observer a hintthat this is for the resource bundle.

We use this approach because the info plist for the resource bundleis too long to manually create. Note that there are severalplaceholders that get filled in during the build process, like thebundle name. All these settings come from the build settings whichwe 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 productsto be named DTPinLockController, but have the targets reflect theiractual purpose.

Because of this I end up with 3 targets with descriptive names like“Demo App”, “Static Library” and “Resource Bundle”. These havedifferent product names “Demo”, “DTPinLockController” and“DTPinLockController” respectively. The different extensions andthe prefix “lib” are automatically added by Xcode.

There are all the modifications we need to do on the build settingsfor the resource bundle:

  • CMD-Backspace on the Base SDK to have it be the same as for theentire 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 getproblems when trying to archive an app using the resource bundle.Since there is no code the compiler-related settings are reallyinconsequential, but I like to have them inherit from the projectsettings to have it look like a native iOS target.

In the build phases of the bundle target there is still a frameworkin “Link Binary with Libraries”. Remove this as well. It does nothurt as there is nothing to link it with, but might beconfusing.

The final step for building the target is now to add the resourcesto it. You select the appropriate files in the project tree and setthe 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. Thiswill create one scheme per target and name them the same as thetargets.

Then we can try and see if the bundle builds correctly.

you can open up each build step and see that Xcode carried out theoptimizations I alluded to above. In the case of this strings fileyou see –outputencoding binary that tells us that the strings filein the bundle will actually be binary.

You now have a DTPinLockController.bundle in the Products groupthat 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 resourceto copy for the app target might get out of sync with the checkmarks in the project navigator. I had to manually go in the buildphases of the demo app target and remove all the references toresources that are now part of the resource bundle.

Since the resource bundle is now a proper target we can add it asdependency to apps using it. This tells Xcode that if thisdependency is somehow “dirty” then the app target is also in needof updating. So it will first build the dependent target and theninclude the product in the app build process.

This shows the Static Library set up as dependency and in “LinkBinary With Libraries”. Now we also want the bundle product to bein the “Copy Bundle Resources” phase. Click the Plus button and addthe bundle product.

This will make the bundle appear in the “Copy Bundle Resources”. Ifit 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 itwill 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 thesame resource bundle as the XIB then you don’t have to changeanything. XIBs will load images from the same bundle that they wereinstantiated from.

However there are a couple of changes you need to make to your codeso that the resources can be found at their new location.

Strings

The default macros for getting localized strings look for thestrings files (aka “string tables”) in the main app bundle. Theseare the definitions of the 4 default macros and you can see thatthe bottom two have a way to specify the bundle to get the stringsfrom 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-youto have a shortcut, but to promote understanding here’s the entirecode that we need to first get an NSBundle instance from ourresource bundle and then get one string from it.

Note that specifying a table of nil means that the strings file iscalled “Localizable.strings”, for a table name of “Name” the fileis called “Name.strings”

// get the resource bundleNSString *resourceBundlePath = [[NSBundle mainBundle] pathForResource:@"DTPinLockController" ofType:@"bundle"];NSBundle *resourceBundle = [NSBundle bundleWithPath:resourceBundlePath]// get a stringNSString *string = NSLocalizedStringFromTableInBundle(@"Set Passcode", @"DTPinLockController", resourceBundle, @"PinLock");

We probably want to have a category on NSBundle specific to ourproject to load and cache in a static variable the resource bundle.And to go with that a localized string macro that hard codes thisresource bundle.

XIBs

You probably have several places where you call [superinitWithNibName:@"MyViewController" bundle:nil]. The nil in thiscase causes the NIB loader to assume that you mean the main bundle.Just as easily we can get the resource bundle and pass there hereinstead.

// get the resource bundleNSString *resourceBundlePath = [[NSBundle mainBundle] pathForResource:@"DTPinLockController" ofType:@"bundle"];NSBundle *resourceBundle = [NSBundle bundleWithPath:resourceBundlePath]// load View Controller from thatUIViewController *vc = [[MyViewController alloc] initWithNibName:@"MyViewController" bundle:resourceBundle];

Do we see a pattern here? You betcha! We always first get theNSBundle instance for the resource bundle and then pass this as aparameter to some method that does something with the resource.

Images

Ah, Graphics. With strings and XIBs both working the same way youwould only be right to assume that there’s a imageNamed:inBundle:method, BUT… it is private. Radar rdar://10250430 byCedric 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 thebundle, like this:

// load image from resource bundleUIImage *image = [UIImage imageNamed:@"DTPinLockController.bundle/Image.png"];

This works because besides being a bundle object the resourcebundle is also folder that can be traversed via its path.

The cool thing about loading XIBs, strings and images from resourcebundles like this is that you retain the awesome powers of thesemethods. Like pathForResource:ofType: will automatically deliverthe correct language version for the device if the resource islocalized. Or imageNamed: will still automatically load Retinagraphics where applicable or device-specific images with ~ipad or~iphone.

I told you before that you don’t need to do anything special ifthese images are referenced from XIBs contained in the sameresource bundle. So this is only necessary for the cases where youload the images from code.

Conclusion

This simplified tutorial has all targets and resource consumers inthe same project. But the same concepts also work if you add thecomponent project as a sub-project.

I hope that I could convince you of the many advantages of resourcebundles over static bundles. I’ve been successfully using them inalmost all of my commercial components and many internalprojects.

Bundle targets allow you to streamline the resource buildingprocess in a way that make larger projects way more effective andless error prone. Make it so!

0 0
原创粉丝点击