Texture Atlases for UIKit with TexturePacker

I use UIKit for my UI, but one issue I was facing is the sheer number of separate image files I was building up in my project. Texture atlas generation is common in OpenGL libraries like cocos2d, and TexturePacker has an export option specifically for these libraries. I couldn’t find anything for UIKit however.

I wrote a quick function to parse TexturePacker’s “Generic XML” data format and generate an NSDictionary of UIImages.

It depends on XMLReader, so you’ll need to include that in your project too.

Updates

  • 13/01/13 – Fixed unnecessary allocation of UIImage which caused a leak.
  • 27/10/13 – Fixed scaling issue on retina devices (thanks Justin!).

UIImage+Sprite.h

// Created by Daniel Sefton, 2012
// Do what you want license
 
#import <Foundation/Foundation.h>
 
/**
 UIImage category to handle parsing of TexturePacker's Generic XML format.
 */
@interface UIImage (Sprite)
 
/**
 The method returns a dictionary of UIImages. Use this function once and reference its contents.
 @param filename the XML file to load, which should be added to your project's bundle
 @returns dictionary of UIImages
 */
+ (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename;
 
@end

UIImage+Sprite.m

// Created by Daniel Sefton, 2012
// Do what you want license
 
#import "UIImage+Sprite.h"
#import "XMLReader.h"
 
@implementation UIImage (Sprite)
 
+ (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename
{
	CGFloat scale = [UIScreen mainScreen].scale;
	NSString* file = [[filename lastPathComponent] stringByDeletingPathExtension];
	if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)] && (scale == 2.0))
	{
		file = [NSString stringWithFormat:@"%@@2x", file];
	}
	NSString* extension = [filename pathExtension];
	NSData* data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:extension]];
	NSError* error = nil;
	NSDictionary* xmlDictionary = [XMLReader dictionaryForXMLData:data error:&error];
	NSDictionary* xmlTextureAtlas = [xmlDictionary objectForKey:@"TextureAtlas"];
	UIImage* image = [UIImage imageNamed:[xmlTextureAtlas objectForKey:@"imagePath"]];
	CGSize size = CGSizeMake([[xmlTextureAtlas objectForKey:@"width"] integerValue], 
		[[xmlTextureAtlas objectForKey:@"height"] integerValue]);
 
	if (!image || CGSizeEqualToSize(size, CGSizeZero)) return nil;
	CGImageRef spriteSheet = [image CGImage];
	NSMutableDictionary* tempDictionary = [[[NSMutableDictionary alloc] init] autorelease];
 
	NSArray* xmlSprites = [xmlTextureAtlas objectForKey:@"sprite"];
	for (NSDictionary* xmlSprite in xmlSprites)
	{
		CGImageRef sprite = CGImageCreateWithImageInRect(spriteSheet, CGRectMake(
			[[xmlSprite objectForKey:@"x"] integerValue], 
			[[xmlSprite objectForKey:@"y"] integerValue], 
			[[xmlSprite objectForKey:@"w"] integerValue], 
			[[xmlSprite objectForKey:@"h"] integerValue]));
		[tempDictionary setObject:[UIImage imageWithCGImage:sprite scale:scale orientation:UIImageOrientationUp] forKey:[xmlSprite objectForKey:@"n"]];
		CGImageRelease(sprite);
	}
 
    return [NSDictionary dictionaryWithDictionary:tempDictionary];
}
 
@end

Usage

self.mySprites = [UIImage spritesWithContentsOfFile:@"mysprites.xml"];
 
UIImage* myImage = [self.mySprites objectForKey:@"myimage"];

7 comments

  1. That’s pretty cool. Having the same problem with tons of resources and getting hard to manage in the project.

    So basically it makes sense to use a spritesheet all graphics it contains where to appear on that single screen right?

    How are you actually using it with UIKit, 1 spritesheet for entire app? have you have any memory issues or overhead?

    Thanks Daniel!

    1. Hi Bach,

      Yes, the idea is that you bunch together multiple images in one spritesheet. It’s up to you if you want to use multiple spritesheets, and that may even be necessary if you have so many. I wouldn’t go any larger than 2048×2048 per spritesheet.

      By default the function pre-allocates all the UIImages at once, which will use more memory but reduce the number of runtime allocations, which is often preferable. I really don’t think it’s an issue, but if you want you can check it out in Instruments.

  2. Texture Packer seems to have been updated and the latest version has native support for UIKit. I updated your code to read the plist file directly instead of the xml. Doesn’t require the XMLReader.


    + (NSDictionary*)spritesWithContentsOfFile:(NSString*)filename
    {
    CGFloat scale = [UIScreen mainScreen].scale;
    NSString* file = [[filename lastPathComponent] stringByDeletingPathExtension];
    if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)] &&
    (scale == 2.0))
    {
    file = [NSString stringWithFormat:@"%@@2x", file];
    }
    NSString* extension = [filename pathExtension];
    NSString * filepath = [NSString stringWithFormat:@"%@", [[NSBundle mainBundle] pathForResource:file ofType:extension]];
    NSDictionary* xmlDictionary = [NSDictionary dictionaryWithContentsOfFile:filepath];
    NSDictionary* xmlTextureAtlas = [xmlDictionary objectForKey:@"meta"];
    UIImage* image = [UIImage imageNamed:[xmlTextureAtlas objectForKey:@"image"]];
    NSString *imageExtension = [[xmlTextureAtlas objectForKey:@"image"] pathExtension];
    CGSize size = CGSizeMake([[xmlTextureAtlas objectForKey:@"width"] integerValue],
    [[xmlTextureAtlas objectForKey:@"height"] integerValue]);

    if (!image || CGSizeEqualToSize(size, CGSizeZero)) return nil;
    CGImageRef spriteSheet = [image CGImage];
    NSMutableDictionary* tempDictionary = [[NSMutableDictionary alloc] init];
    NSDictionary * xmlSprites = [xmlDictionary objectForKey:@"frames"];
    for (id key in xmlSprites)
    {
    NSDictionary * xmlSprite = [xmlSprites objectForKey:key];
    CGRect unscaledRect = CGRectMake([[xmlSprite objectForKey:@"x"] integerValue],
    [[xmlSprite objectForKey:@"y"] integerValue],
    [[xmlSprite objectForKey:@"w"] integerValue],
    [[xmlSprite objectForKey:@"h"] integerValue]);
    CGImageRef sprite = CGImageCreateWithImageInRect(spriteSheet, unscaledRect);
    // If this is a @2x image it is twice as big as it should be.
    // Take care to consider the scale factor here.
    NSString * imageName = [NSString stringWithFormat:@"%@.%@", key, imageExtension];
    [tempDictionary setObject:[UIImage imageWithCGImage:sprite scale:scale orientation:UIImageOrientationUp] forKey:imageName];
    CGImageRelease(sprite);
    }

    return [NSDictionary dictionaryWithDictionary:tempDictionary];
    }

Leave a Reply to Justin Cancel reply

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