I’ve recently been working on a static library for distribution to other developers — Audiobus — and I need to include a couple of graphical resources with the distribution. The usual solution to this is to include the resources separately in a bundle, and require the user to drop them in to their project along with the static library.
I thought I’d see if I could make the process just a little neater, and successfully devised a way to compile the images straight into the library, so the distribution remains nice and clean — just the library itself and a few header files.
Now, I can pop image resources into a folder, and after compiling, access them within the static library with:
UIImage *image = TPGetCompiledImage(@"Button.png"); |
It automatically handles “@2x” Retina images (although it doesn’t currently do “~ipad” versions).
Here’s how it’s done.
The magic is in a shell script which uses the xxd
hex dump tool to create C code that represents the image data as a byte array, then creates around it a set of utilities to turn those arrays into UIImages on demand.
Along with it is a couple of template files — a header and implementation file — that describe the format of the derived code.
Finally, a little tweaking of the project in Xcode (with a brief foray into a text editor to work around some Xcode shortcomings) puts it all together.
Update: Fellow dev Cocoanetics pointed out that they’d solved a similar problem, and have a great write-up on how they create compiled resources using custom build rules on their blog.
The Image Resources
…Just a bunch of png
files within a folder inside your project directory. The script assumes there are normal and retina (@2x
) versions of each.
The Template
These are the template source files from which the end result will be derived. It contains a few tags that the accompanying shell script will process. I created it in Xcode, placing it within the same folder as the source png images, but removed it from the target’s compile phase, as we’ll be adding the derived source instead.
First the header, TPCompiledResources.h
:
// // TPCompiledResources.h // // Created by Michael Tyson on 13/05/2012. // Copyright (c) 2012 A Tasty Pixel. All rights reserved. // #import UIImage *TPGetCompiledImage(NSString* name); |
And the implementation file, TPCompiledResources.m
:
// // TPCompiledResources.m // // Created by Michael Tyson on 13/05/2012. // Copyright (c) 2012 A Tasty Pixel. All rights reserved. // #import "TPCompiledResources.h" /*{%IMAGEDATA START%}*/ /*{%IMAGEDATA END%}*/ UIImage *TPGetCompiledImage(NSString* name) { /*{%LOAD_TEMPLATE%} if ( [name isEqualToString:@"ORIGINAL_FILENAME"] ) { static UIImage *_SANITISED_FILENAME_image = nil; if ( _SANITISED_FILENAME_image ) return _SANITISED_FILENAME_image; if ( [[UIScreen mainScreen] scale] == 2.0 ) { _SANITISED_FILENAME_image = [[UIImage alloc] initWithCGImage: [[UIImage imageWithData:[NSData dataWithBytesNoCopy:SANITISED_2X_FILENAME length:SANITISED_2X_FILENAME_len freeWhenDone:NO]] CGImage] scale:2.0 orientation:UIImageOrientationUp]; } else { _SANITISED_FILENAME_image = [[UIImage alloc] initWithData:[NSData dataWithBytesNoCopy:SANITISED_FILENAME length:SANITISED_FILENAME_len freeWhenDone:NO]]; } return _SANITISED_FILENAME_image; } {%LOAD_TEMPLATE END%}*/ /*{%IMAGELOADERS START%}*/ /*{%IMAGELOADERS END%}*/ return nil; } |
The Shell Script
Here’s the script that does all the work. The script looks for all “png” images in the given folder, then creates C code representing each image along with wrapper code to give access to the image byte arrays, with help from the template.
This script goes into a “Run Script” phase, placed at the beginning of the library’s build process.
#!/bin/sh # Where the images are (get this from the first "Input Files" entry) RESOURCES_FOLDER=`dirname "$SCRIPT_INPUT_FILE_0"` # The name of the source template, minus extension SOURCE_NAME="TPCompiledResources" # Create C arrays, representing each image tmp="$TEMP_FILES_DIR/compile-images-$$.tmp" cd "$RESOURCES_FOLDER" for image in *.png; do xxd -i "$image" >> $tmp.1 done # Read the code template TEMPLATE=`sed -n '/{%LOAD_TEMPLATE%}/,/{%LOAD_TEMPLATE END%}/ p' "$RESOURCES_FOLDER/$SOURCE_NAME.m" | sed '1 d;$ d'` # Create loader code for each image for image in *.png; do if echo "$image" | grep -q "@2x"; then continue; fi ORIGINAL_FILENAME="$image" SANITISED_FILENAME=`echo "$ORIGINAL_FILENAME" | sed 's/[^a-zA-Z0-9]/_/g'` SANITISED_2X_FILENAME=`echo "$SANITISED_FILENAME" | sed 's/_png/_2x_png/'` echo "$TEMPLATE" | sed "s/ORIGINAL_FILENAME/$ORIGINAL_FILENAME/g;s/SANITISED_FILENAME/$SANITISED_FILENAME/g;s/SANITISED_2X_FILENAME/$SANITISED_2X_FILENAME/g" >> $tmp.2 done # Create the source file from the template and our generated code sed "/{%IMAGEDATA START%}/ r $tmp.1 1,/{%IMAGEDATA START%}/!{/{%IMAGEDATA END%}/,/{%IMAGEDATA START%}/! d;} /{%IMAGELOADERS START%}/ r $tmp.2 1,/{%IMAGELOADERS START%}/!{/{%IMAGELOADERS END%}/,/{%IMAGELOADERS START%]/! d;}" "$RESOURCES_FOLDER/$SOURCE_NAME.m" > "$DERIVED_FILE_DIR/$SOURCE_NAME.m" # Copy the template header file in cp "$RESOURCES_FOLDER/$SOURCE_NAME.h" "$DERIVED_FILE_DIR/$SOURCE_NAME.h" rm "$tmp.1" "$tmp.2" |
The “Run Script” phase that hosts this script also needs a couple of additions, to tell Xcode what the inputs and outputs to the script are: In the “Input Files” section, the path to the image resource folder, with a “**.png*” wildcard at the end, and also the path to those template files, TPCompiledResources.{h,m}
. Finally, the two output files go in the “Output Files” section:
Project Setup
Now it was just a matter of setting up the project to include the derived source files in the build. This was a bit messy and took a little doing, but with guidance from this article by Ben Zado on file references relative to DERIVED_FILE_DIR, it wasn’t too painful:
- Build, in order to generate the derived source files.
- Navigate to the derived sources folder within the build products, and drag the
TPCompiledResources.{m,h}
into the project, placed within a new group (I called the group “Derived Sources”). - The path type for those files (accessible from the properties viewer — Cmd-I) should be “Relative to Enclosing Group”, and consequently the “Path” field should just show the filename, with no path component. This was a bit touch-and-go for me, and I had a hard time making this happen, so I left it as-is for now, fixing it manually later in step 8.
- Under Xcode Preferences » Locations » Source Trees, add an entry with setting name “
DERIVED_FILE_DIR
“, display name “Derived Files” and path “$(DERIVED_FILE_DIR)
“. - Set the path type for the group containing the two derived sources to “Relative to Derived Files”.
- Quit Xcode, and open the
project.pbxproj
file from within your project’s bundle. - Find the “Derived Sources” group (or whatever it was named in step 2), and delete the “path” property from the list of attributes.
- I had to also find the
TPCompiledResources.{m,h}
file sections and delete the “path” attribute for those, too. - Reopen Xcode, and build — it should be good to go (don’t worry that the derived sources are shown in red in the project group — Xcode’ll find them).
Now that’s done, images can be accessed by TPGetCompiledImage(@"ImageName.png")
. Yay!
Hey Michael,
However, when I include my static library in another project, a linker error is thrown saying
Undefined symbols for architecture i386:
“_TPGetComipledImage”, referenced from:
And points to where I reference it in the static library. I’ve tried to backtrack the problem but all I can think of it having to do is xcode’s inability to find the file because of the change of the .xcodeproj settings. Any ideas?
After attempting to run it on a phyiscal iPad, its throwing an Undefined symbols for armv7 error as well. Which furthers my suspicion the symbols somehow aren’t being included even its compiling.
Will circle back with any findings.
Cheers Tenny! Sorry about my silence – I’m afraid I haven’t a clue why it’s not working for you. Let me know how you go, though.
The reason its broken is that … Apple “forgot” to test Xcode 4.4, Xcode 4.5, and iOS 6 … with the iPhone5.
Xcode (as a tool suite) “does not support” the iPhone5 processor. You can still make apps with it, but you have to disable some of the build steps and replace them with kludged alternatives.
In this case, the problem is lipo: it doesn’t know what the armv7s architecture is, and … (maybe because it was badly written in the first place? No idea, just guessing) … it craps-out in spectacular fashion, giving the wrong error message as it does so.
c.f. latest update to http://stackoverflow.com/a/3647187/153422
As an aside: I’d recommend looking at Karl Stenerud’s complete, total, “Frameworks in iOS” setup – it lets you create real Frameworks (which Apple for the past 4 years has left hardcoded “disabled” in Xcode, for no apparent reason), so issues like this go away.