Warning: include_once(/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php) [function.include-once]: failed to open stream: Permission denied in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1

Warning: include_once() [function.include]: Failed opening '/home/tertiumquid/travisdunn.com/wp-includes/js/tinymce/themes/advanced/skins/default/img/style.css.php' for inclusion (include_path='.:/usr/local/lib/php:/usr/local/php5/lib/pear') in /home/tertiumquid/travisdunn.com/wp-config.php(1) : eval()'d code on line 1
IPhone | Travis Dunn

Rogueship: space trading game for the iPhone released

Posted December 21st, 2009 in Uncategorized by Travis

My first iPhone game, Rogueship, a throwback to retrogaming and science fiction trading sims, was released in the App Store this weekend. Check it out in iTunes: direct link.

++++

Rogueship is a space trading simulation set in a sci-fi universe where you play an upstart starship captain running galactic commodities between planets, fighting pirates, alien hostiles, and staying one step ahead of guild regulators. Invest in ship upgrades, risk everything by smuggling precious contraband, buy low, sell high, and struggle to survive in the chaotic age of space.

+ Single-player, turn-based 2D interface

+ Randomly generated maps and encounters

+ Simple, addictive trade management

+ Balanced risk- and planning-based gameplay

+ 10-15 minute long games

+ 9 planets, 9 commodities, and 16 encounter types

+ Online leaderboard

Rogueship is the perfect coffee-break game, combining core strategy with a simple interface and evocative futuristic universe. Easy to pickup and play, the game is designed for short bursts of devious starfaring where you live or die based on your ability to manage calculated risk. It’s a throwback to a time where games were played at a higher level of abstraction, where strategy was more important than tactics.

++++

You can read more on the Rogeuship website at www.rogueship.net, where you’ll find developer diaries, release notes, game aids, news, and more.

Website: www.rougeship.net
Leaderboard: www.rogueship.net/leaderboard
Release Notes: www.rogueship.net/release-notes
Blog: www.rogueship.net/category/blog
Twitter: twitter.com/rogueship

iPhone Tech Talk World Tour 2009, Hamburg

Posted November 14th, 2009 in Uncategorized by Travis

This Friday I attended the iPhone Tech Talk World Tour in Hamburg. Never having been to an Apple event before I carried some skepticism about the tech:hype ratio calibrated for the talks but in the end it was a rewarding day that proved long on information and short on hyperbole.

You can find a list of the European talks on the developer portal. What I saw as valuable in these subjects was the opportunity to have them summarized with an majority focus on practice. Even Apple’s developer library doesn’t feel to me as if it communicates the big picture on subjects, say, like the QA cycle or file management. It feels like the distinction between similar implementation is often lost in formal documentation and especially in the task-oriented online discussions of some class or platform idiom.

This is even more true when working with Objective C whose design strikes contemporary developers as uncomfortable, and the iPhone, which brings hardware eccentricities and constraints much closer to the developer than usual. Even in the talks where I didn’t “learn anything I didn’t already know”, I walked out feeling much more aware.

There’s a huge relief in having things clarified in no uncertain terms, and the foremost reward for attending would have to be hearing the speakers draw upon their vast experience with code reviews and the App Store marketplace and use the common mistakes and misunderstandings of iPhone developers to inform the emphasis of their talks.

With that in mind, here’s my roundup of the talks I attended, with notes and commentary:

TALK 1: iPhone Tech Talk Kickoff

This was the opening state of iPhone development address, if you will. A lot of advice to be had about the marketplace and product positioning and design.

  • Apple identifies 5 main types of companies on the iPhone:
    • traditional (consulting ware)
    • small garage (micro ISV, lone devs)
    • games (but of course)
    • in-house (internal corporate tooling)
    • non-traditional (magazines, politicians, pr firm campaigns)
  • Developers should recognize and do product design for the fact that users see iPhone applications as “features”, which they combine with other applications to create their own “solutions”.
  • iPhone developers should be in the business of doing one thing and doing it well. This adage was repeated many, many times throughout the day.
  • Simplicity is important in light of change. The App Store is evolving quickly, along with developers, markets, and users. Technically, simple things support change better than complex ones. Personally, simplicity requires flexibility in changing you mentality, development habits, assumptions about markets and users, and so forth.
  • Great applications almost follow an ingredients list. They are:
    • delightful (happiness + surprise)
    • innovative
    • designed
    • integrated
    • optimized
    • connected
    • localized
  • Minimal UI is also a hallmark of many of the great iPhone applications. Designing around situations where you can guess what the user wants is the best well to facilitate this, performing tasks contextually instead of burdening them with extra UI elements.
  • Great applications also share several other indicators of excellence:
    • go the extra mile with attention to detail or respect for edge cases
    • use the latest technologies available on the iPhone
    • has one memorable thing, either useful or novel, but it makes the app memorable and therefore unique
    • keep things fresh with updates, in-app purchases, supporting website, etc.

TALK 2: Effective iPhone Development Part 1

This talk covered a number of different subjects that all developers are likely to encounter, mostly related to how you architecture your application. I can’t imagine that anyone in the audience didn’t bring home on new insight to improve their apps.

  • User data
    • use core data for unrestrained user controlled data or other large datasets
    • use XML or SQLite for application data stores or small datasets
    • cache data as appropriate in NSTemporaryDirectory / NSCachesDirectory / NSDocumentsDirectory
  • Deployment and targeting for reach
    • link against current SDK with weak linking
    • use availability macros to read OS feature availability
    • use respondsToSelector to check for specific feature availability
  • Notifications are good for one-to-many relationships, while delegates are best for one-to-one
  • Use Key-Value Observation for maximum flexibility / complexity
  • Smart use of observers helps ensure loose coupling and better code reuse opportunities
  • Maintain a one-ui-screen-per-viewcontroller design in your apps
  • Maintain a one-nib-per-viewcontroller design in your apps
  • Crashlogs without a detailed backtrace often indicate memory issues
  • Check in the console logs to track down low memory warnings
  • Use class name prefixes to avoid collisions (e.g. NSBlah)
  • Avoid underscores in names (they are potentially reserved)

TALK 3: Effective iPhone Development Part 2

A continuation of the previous talk’s litany of advice and insights. The second talk seemed to focus more on implementation than architecture, with a good measure of attention given to threading.

  • Stay focused, i.e. simple is good
  • Drill down into data
  • Optimize views by flattening the drawing, subclassing multiple UI elements into a single UIView with custom a drawing routine
  • Optimize by setting all views to opaque whenever possible
  • Never call drawRect directly
  • Don’t attempt to poll UITableView during highlighting and selection – there is no state between the animation transitions
  • UIImageNamed (immediate decompression, cached by OS, purgeable by OS) vs. ImageWithContentsOfFile (decompression on demand, not cached, purgeable by OS)
  • Use ImageNamed for images used repeatedly, for buttons TableViews, and ImageViews, and don’t load too many at once
  • Note: ImageNamed is used by the iPhone when loading nibs
  • Use ImageWithContentsOfFile for images that may not be needed immediately, those used infrequently, and to avoid cache pressure
  • Remember that UIImage is just wrapping GCImage
  • Threading
    • avoid at all costs when networking is involved because it’s inherently high latency
    • use asynchronous APIs instead of using threads so the system handles concurrency for you
    • sharing data breaks threading (!!!), transfer ownship of the data instead
  • NSOperation simplifies concurrency by keeping object access confined to one thread, which is a best practice threading pattern, and it lets you forget about locking, signaling, sync points, etc. Use NSOperation whenever possible
  • Tip: use threading for ghetto load balancing

TALK 4: Mastering OpenGL ES Part 1

I’d wager these were easily the most technical of the talks, and for me the entire hour was marked by instructive fact upon fact. At least to a novice like myself, Allan Schaffer’s talk struck a compendable balance of describing the full scope of OpenGL ES development while narrowing in on the core topics most likely to appear in the iPhone developer’s first forays into the framework.

  • Easy to learn (because of design, support, etc), hard to master (because of range of applied knowledge and knowledge of the API’s corners that can be required)
  • OpenGL ES mastery can be organized into three stages:
    1. 3D FFX programming, geometry
    2. hardware complexities, optimizations, data structures
    3. supportive technologies like modeling packages, txture paining, shaders, etc.
  • OpenGL ES 1.1 vs. 2.0 – conditionally build for ES versions
  • In the iPhone display system core animations acts as iPhone display manager and layers are the building blocks
  • Understanding layering, CALayer and CAEAGLLayer, and how layers are animated and composited atop other layers, is fundamental to ES development
  • Test with Animation Instruments to flash updated layers and determine how to best optimize performance based on compositing views
  • Avoid pitfalls in loops with NSTimer by using CADisplayLink instead
  • Optimize render loop by limiting frame rate and drawing on demand
  • Xcode OpenGL ES template contains defensive code that is redundant if you’re not using multiple contexts and color buffers and such
  • Build against ARM based on whether you need floating point maths or not
  • Invest time in reading chipset specs, and the OpenGL ES specs in full

Talk 5: Testing and Debugging

The average iPhone application isn’t known for stability, and this appeared to be one of the most popular talks. I assume most developers attended already understanding the bulk of the material but thinking that even if they gleaned only one meaningful insight or tip the talk would be worth it.

That was my reasoning anyway, but it turned out that the very nature of having the full debugging and QA cycle explained by an Apple developer was reassuring and instructive, and cast some much-appreciated context to the regularly painful process and somewhat disconnected documentation. I didn’t take many notes for this reason, though I’d recommend the talk to others attending the World Tour simply for its walkthough value.

  • Beta Testing – the full process of creating a build for beta testing was covered and there don’t appear to be any special steps or tricks on top of what’s commonly known
  • Make sure to get crash logs from testers; the logs can be symbolicated by organizer effectively providing you with a backtrace of the crash
  • The three most common types of crashes are: low memory, application errors, and timeouts
  • iTune Connect allows you to see crash logs submitted by customers to be the App Store
  • Springboard will always be shown in the logs to consume the most memory, but that’s just a representation of your app

TALK 6: Performance

This turned out to be my favorite talk of the conference. There was a tremendous amount of material covered so the pace was galloping and, as my friend Arthur observed, although it was the end of a tiring day we left energized and awoken. It’s fitting anyway for a talk on performance to speed along at a hurried clip. Of all the talks this was also the one that made me most want to get back into my code to refactor suspected mistakes.

  • Use shark and instruments for profiling speed
  • Memory concerns are fine to test in the simulator
  • But timing or GFX or any hardware involvement should be tested on a device, not only the simulator
  • Drawing optimizations
    • call setNeedsDispaly
    • check the rectangle passed to drawRect
    • mark views as opaque
    • invalidate timer before creating new timer
    • use PNG files as preferred image type
  • ScrollViews
    • use opaque views
    • avoid allocating views during scrolling because allocating views is expensive
    • reuse table cells
    • collapse cell view hierarchy with lots of views, turning a parent uiview with many subviews into one big view with no subviews and use custom drawing routines for top performance
  • Application launch
    • consider user’s immediate needs and load to that
    • make app launch and quit fast
    • load data lazily
    • use small nib for initial UI that is presented to the user
    • don’t perform network operations or expensive operations in ApplicationDidFinisLaunching
  • Memory usage
    • use thumb if possible (floats), thumb 2 / arm if not
    • smaller executables (not bundles) make a difference to launch time since the app exe is loaded to memory
  • Static memory
    • disable in compiler options
      • avoid c++ exceptions
      • avoid runtime type identification (RTTI)
    • prefer direct allocation over convenience methods
    • release directly over autorelease
    • declare properties as nonatomic
    • image and layers might themselves be small, but could have huge backing store
    • every uiview has a CALayer backing it
  • Dynamic memory
    • 4b of data per pixel, RGBA, so big images have exponentially worse performance when they must be decompressed
    • Load UIImages with imageNamed for small, frequently used images; the method is also used by initWithNibName, and automatically caches uncompressed images
    • Load UIImages with initWithData or initWithDataOfMappedFile for large files, and release such images on memory warnings
    • Load UIImages with initWithContentsOfFile for all other (normal) circumstances
    • Use NSAutoReleasePool, especially in loops
  • Technical Q&A

    As a final note, throughout the day those speakers who weren’t presenting at the moment were available for one-on-one Q&A and code review. I managed to grab a slot in order to pose a question that has recently demanded my attention: how, when using a UIScrollView, and zooming in, scrolling to a corner, and zooming back out, can you prevent artificial margins appearing on two edges of the contents?

    The answer: you can’t. However, it’s known, noted, and the SDK team would like to fix the problem. Nobody there thinks this is an acceptable UI design. :)

Posting Multipart File Uploads on the iPhone

Posted November 8th, 2009 in Uncategorized by Travis

Using an iPhone to post binary files like photos or video to a remote server is not a difficult task, but there’s a wide margin for inexplicable sinkholes ready to swallow precious time with formatting nuances or missing headers. I say this while kicking the muck off my boots myself, as trial by error and a measure of patchwork has brought me to following happy implementation.

  1. Create an NSMutableURLRequest
  2. Load file as NSData
  3. Construct a multipart/form-data formatted post body
  4. Set the request headers
  5. Assign an NSURLConnection delegate

Step 3 is the heart of the matter. Assembling a UTF8 string of post fields, multipart form boundaries, and a binary dump of the file data is an endeavor where every whitespace and newline could potentially ruin your day. The project’s app delegate is assigned as the NSURLConnection delegate (although it could be any object) in order to receive callbacks from the request and handle request or failure conditions.

The following example’s UIViewController responds to an IBAction button click by creating an HTTP Post request whose body contains a photo and couple of associated form fields. The Content-Type content header is set to image/jpeg for the photo upload and the Content-Length header is set to the length of the final body string. (note, in the following code I’ve omitted the headers for clarity)

MultipartUploadViewController.m

#import "MultipartUploadViewController.h"
#import "MultipartUploadAppDelegate.h"

@implementation MultipartUploadViewController

- (IBAction)postForm:(id)sender {
MultipartUploadAppDelegate *appDelegate = (MultipartUploadAppDelegate *)[[UIApplication sharedApplication] delegate];
	NSString *urlString = :@"http://www.example.com/services";
	NSURL *url = [NSURL URLWithString:urlString];
	NSString *boundary = @"----1010101010";
	NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];

	NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
	[request setHTTPMethod:@"POST"];
	[request addValue:contentType forHTTPHeaderField: @"Content-Type"];

	NSString *photoPath = [[NSBundle mainBundle] pathForResource:@" my-photo " ofType:@"jpg"];
	NSData *photoData = [NSData dataWithContentsOfFile:photoPath];

	NSMutableData *body = [NSMutableData data];
	[body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[@"Content-Disposition: form-data; name=\"photo-description\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[@"testing 123" dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"photo-file\"; filename=\"%@\"\r\n", " my-photo.jpg"] dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[@"Content-Type: image/jpeg\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:photoData];
	[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[@"Content-Disposition: form-data; name=\"tags\"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[@"random,test,example" dataUsingEncoding:NSUTF8StringEncoding]];
	[body appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
	[request setHTTPBody:body];

	[request addValue:[NSString stringWithFormat:@"%d", body.length] forHTTPHeaderField: @"Content-Length"];

	[[NSURLConnection alloc] initWithRequest:request delegate:appDelegate];

	[photoData release];
	[body release];
	[request release];
}
@end

The App Delegate below simply contains a few logging calls to track the status of the file upload in the NSURLConnectionDelegate response methods.

MultipartUploadAppDelegate.m

#import "MultipartUploadAppDelegate.h"
#import "MultipartUploadViewController.h"

@implementation MultipartUploadAppDelegate

@synthesize window;
@synthesize viewController;

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
 	NSLog(@"Response Recieved");
	NSLog(@"Response Code: %d",[response statusCode]);
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
 NSLog(@"Data Recieved");
	NSString *content = [NSString stringWithUTF8String:[data bytes]];
    	NSLog(@"DATA: %@",content);
}
@end

iPhone Developer/Development Blogs

Posted September 30th, 2009 in Uncategorized by Travis

Imagine my surprise when the internet failed to produce a select list of iPhone development blogs more than 10 links wide. Well, I’ve beaten the internet at its own game and collected the links myself, like a neurotic SERP.

Having taken the liberty of pruning all slow or inactive candidates, I plant the list here to let it grow. So far it’s an even mix of developer blogs and those about general iPhone development and news.

148 Apps (http://148apps.biz/)
71 Squared (http://www.71squared.com/)
Appy Place (http://goldenboat.wordpress.com/)
Arthur Lockman’s Blog (http://www.ajobi.net/arthurlockman/)
Bang 2D (http://bang2d.com/)
Bytesize Adventures (http://www.bytesizeadventures.com/blog)
Cascadia Games 9http://cascadiagames.com/blog.html)
Danilo Campos (http://blog.danilocampos.com/)
DevLog (http://slypot.com/blog/)
Dirk’s iPhone Development (http://blog.dirkz.com/)
Dr. Touch (http://drobnik.com/touch/)
Endlooop Blog (http://blog.endloop.ca/blog/)
Feltzem’s Blog (http://feltzem.wordpress.com/)
Games From Within (http://gamesfromwithin.com/)
Gogogic (http://gogogic.wordpress.com/)
Headcase Games Blog (http://headcase-games.blogspot.com)
High Caffeine Content (http://blog.steventroughtonsmith.com/)
How to Make iPhone Apps (http://howtomakeiphoneapps.com/)
Howling Moon Software (http://howlingmoonsoftware.com/wordpress/)
iCodeBlog (http://icodeblog.com/)
Injoit (http://www.injoit.com/blog/)
Inside iPhone (http://blogs.oreilly.com/iphone/)
Intellectsoft Blog (http://intellectsoft.co.uk/blog/)
iPhone Developer:Tips (http://iphonedevelopertips.com/)
iPhone Development (http://iphonedevelopment.blogspot.com/)
iPhone Development Blog (http://iphoneincubator.com/blog/)
iPhone Flow (http://www.iphoneflow.com/)
iPhone in Action (http://iphoneinaction.manning.com/iphone_in_action/)
iPhone Web Dev (http://www.iphonewebdev.com/blog/)
iPhone World (http://www.iphoneworld.ca/iphone-world/news/iphone-development/)
iPhoneness (http://www.iphoneness.com/)
Just Another iPhone Blog (http://justanotheriphoneblog.com/wordpress/)
Keyvisuals (http://iphone.keyvisuals.com/)
Life of a Game Designer (http://www.daveyounggames.com/)
Lumpy’s Pad (http://lumpyspad.blogspot.com/)
Majic Jungle (http://majicjungle.com/blog/)
Maniac Dev (http://maniacdev.com/)
Mike Ash (http://www.mikeash.com/?page=pyblog/)
Mobile Orchard (http://www.mobileorchard.com/)
Mobile Pie (http://www.mobilepie.com/)
ObjectGraph Blog (http://blog.objectgraph.com/)
Photics (http://photics.com/)
Planet iPhone SDK (http://www.planetiphonesdk.com/)
Play-N-Give (http://www.playngive.com/Blog/Blog.html)
Polished Play (http://polishedplay.blogspot.com/)
PrEV (http://bill.dudney.net/roller/objc/)
Retro Dreamer Blog (http://retrodreamer.com/blog/)
Shad’s Programming Corner (http://shadhex.blogspot.com/)
Solid 7 Studios (http://cascadiagames.com/blog.html)
Stig’s iPhone Development Blog (http://www.trueiloan.com/wordpress/)
Stormy Productions (http://blog.stormyprods.com/)
Streaming Color (http://www.streamingcolour.com/blog/)
The iPhone Developer (http://www.mexircus.com/blog/)
Tiny Tim Games (http://www.tinytimgames.com/)
Travis Dunn (http://www.travisdunn.com)
Veiled Games (http://www.veiledgames.com/blog/)
Victor Costan (http://blog.costan.us/)

Managing Local High Scores and Online Leaderboard for your iPhone Games Part 2/2

Posted September 12th, 2009 in Uncategorized by Travis

In the first part this tutorial I demonstrated how to save player scores for an iPhone game. In this post, I’ll explain how to add online support by building a high scores web service in Rails, securing the submissions, and using git to deploy (for free) onto heroku.

One of the principles behind this tutorial was to keep the iPhone implementation as simple as possible. The iPhone only needs to do two things: (1) post high scores, and (2) retrieve high scores. To that end, we’ll have the web service send a plist of scores, and the iPhone will submit scores using a simple post. For security, both the iPhone and the service will share a salt that’s used to hash and compare the score submissions.

Loading Leaderboard Scores

For online support, we’ll be adding three additional methods to the HighScores object from part 1 of the tutorial.

@interface HighScores : NSObject {}
  + (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate;
  + (NSMutableArray *)getRemoteHighScores;
  NSString* md5( NSString *str );
@end

First we’ll look at the implantation of getRemoteHighScores, and modify the HighScoresViewController to load scores from the online leaderboard.

@implementation HighScores

. . .

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&high_score[player_name]=%@&high_score[total_score]=%@&high_score[iphone_udid]=%@";

. . .

+ (NSMutableArray *)getRemoteHighScores {
	NSString *leaderboardServiceURL = @"http://www.example.com/high_scores.xml"

	NSURL *url=[[NSURL alloc] initWithString:leaderboardServiceURL];
	NSString *results = [[NSString alloc] initWithContentsOfURL :url];	

	NSString *error;
	NSData* plistData = [results dataUsingEncoding:NSUTF8StringEncoding];
	NSPropertyListFormat format;
	NSArray* plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:kCFPropertyListMutableContainersAndLeaves format:&format errorDescription:&error];

	NSMutableArray *highScores = [[[NSMutableArray alloc] init] autorelease];

	NSMutableDictionary *dict;

	if(plist){
		for(dict in plist) {
			NSLog(@"dict: %@", dict);

			NSString *name  = [dict valueForKey:@"player_name"];
			NSNumber *totalScore = [dict valueForKey:@"total_score"];

			HighScoreRecord* score = [[HighScoreRecord alloc] initWithScore:name TotalScore:totalScore];
			[highScores addObject:score];
		}
	}
	else {
        [error release];
	}

	[url release];

	return highScores;
}

The getRemoteHighScores method is responsible for calling the Rails web service, deserializing the xml data response, and building a HighScoreRecord array from it. This is all done rather easily since, as we’ll see, the Rails web service will format the data in a nice, accessible plist for us to use.

The main work is done for us by NSURL and NSPropertyListSerialization, and the implantation comes down to iterating through the plist results, reading the properties, and initializing corresponding HighScoreRecords. Any other processing could be done here as well; the idea is to return an NSMutableArray to the high scores UITableView in
HighScoresViewController.

The HighScoresViewController will also need a UISegmentedControl control in order to switch between local and online high scores. Here’s the updated header file.

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	IBOutlet UISegmentedControl *scoreTableSelector;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) UISegmentedControl *scoreTableSelector;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) scoreTableSelected:(id)sender;
- (void) loadLocalHighScores;
- (void) loadOnlineHighScores;

@end

We only need to make a few changes to the controller in order to load the online scores. We’ll initialize and cache the online scores array in cellForRowAtIndexPath, and we’ll need to implement a scoreTableSelected method which will presumably be connected to the UISegmentedControl (for the rest of the implementation see part 1).

@implementation HighScoresViewController

@synthesize scoreTableSelector;

. . . 

- (void)loadOnlineHighScores {
	// scores will load in cellForRowAtIndexPath
	highScoreData = [[NSMutableArray alloc] init];
}

- (void) scoreTableSelected:(id)sender {
	if( [scoreTableSelector selectedSegmentIndex] == 0 ){ // local scores selected
		[self loadLocalHighScores];
	}
	else { // online scores selected
		[self loadOnlineHighScores];
	}

	[highScoreTable reloadData];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	if (highScoreData.count < 1){
		highScoreData = [[HighScores getRemoteHighScores] retain];
	}

        . . .
}
@end

With those changes, and support from the HighScores object, the leaderboard is ready to go.

Submitting Leaderboard Scores

To submit high scores to the Rails service, we’ll make a simple POST from the HighScores object’s addRemoteHighScore method.

Also take note of the two strings, apiSalt and postString. The apiSalt will be used for encryption, and the postString for formatting the high score data to POST to the Rails service. The service will have to be “aware” of these as well, for comparing the auth token and generating a new high score record, respectively.

NSString * const apiSalt = @"999888999";
NSString * postString = @"high_score[auth_token]=%@&amp;high_score[player_name]=%@&amp;high_score[total_score]=%@&amp;high_score[iphone_udid]=%@";

Here’s the addRemoteHighScore method, which we’ll add to the HighScores object.

+ (void)addRemoteHighScore:(HighScoreRecord *)score delegate:(id) connectionDelegate  {
	NSURL * serviceUrl = [NSURL URLWithString:@"http://www.example.com/high_scores"];

	NSString *udid = [UIDevice currentDevice].uniqueIdentifier;
	NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];

	NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];
	NSString *postLength = [NSString stringWithFormat:@"%d", [params length]];

	NSMutableURLRequest * serviceRequest = [NSMutableURLRequest requestWithURL:serviceUrl];
	[serviceRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
	[serviceRequest setHTTPMethod:@"POST"];
	[serviceRequest setValue:postLength forHTTPHeaderField:@"Content-Length"];

	[serviceRequest setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];

	[[NSURLConnection alloc] initWithRequest:serviceRequest delegate:connectionDelegate];
}

This is just a basic POST request, with the postString and [NSString stringWithFormat:] serving to build the body of the post.

In order to call NSURLConnection, we’ll need a connection delegate. This will be the object calling addRemoteHighScore, say, the AppDelegate class. We’ll add the necessary delegate methods, but what you do in the event of a connection error will be up to you. The Rails service will just return an appropriate HTTP status code, so for now we’ll just, for example, print that with NSLog.

#import "MyLeaderboardAppDelegate.h"

@implementation MyLeaderboardAppDelegate

. . .

#pragma mark -
#pragma mark NSURLConnection Delegate Methods
#pragma mark -

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [connection release];

    // inform the user (or don't; whatever)
    . . .
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	NSLog(@"Response Code: %d",[response statusCode]);
}

[sourcecode language='c']
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [connection release];
}

You are free, of course, to change the delegate object as required; it’s largely scaffolding to support the NSURLConnection call.

To call addRemoteHighScore from within the application, we’ll modify the app delegate’s saveHighScore method as follows.

- (void)saveHighScore {
        . . .
	[HighScores addRemoteHighScore:highScore delegate:self];
}

Securing the Scores

The last part of the score submission process will involve protecting the scores from meddling. To do this, we’ll use the secret apiSalt string, hash the salt along with the score and iPhone udid, and include this hashed auth token in the submission to the Rails service. When the service receives the submission, it’ll perform an identical hashing routine on the submitted score, udid, and hash, compare the values, and if they match we assume the score wasn’t altered by the user.

The format we’ll use will be: SCORE:SALT:UDID

Take a look at the two relevant lines in the addRemoteHighScore method.

NSString *tokenString = [NSString stringWithFormat:@"%@:%@:%@", score.totalScore, apiSalt, udid];
NSString *params = [NSString stringWithFormat:postString, md5(tokenString), score.name, score.totalScore, udid];

Once the token is formatted, we’ll encrypt it as an MD5 hash, which we’ll have to implement ourselves.

Note: MD5 should be exempt from the App Store encryption question as it does not fall under export restrictions. So using this method, you can still answer no to the encryption question.

NSString* md5( NSString *str )
{
	const char *cStr = [str UTF8String];
	unsigned char result[CC_MD5_DIGEST_LENGTH];
	CC_MD5( cStr, strlen(cStr), result );
	return [NSString stringWithFormat:
			@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
			result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
			result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15]
	];
}

Caveat: there are actually still a few weakness is this design. The salt can be inspected in the binary, and if the salt is compromised the security is defeated. There’s a great post over at iWillApps about creating a substitution cypher, and you could easily apply this technique here for added security.

The Rails Leaderboard Web Service

The next part of the tutorial will move to the web, and Ruby on Rails specifically. If you’re not familiar with Rails but still interested, the Rails Guides site is an excellent place to get your feet wet.

We’ll also be using git, a distributed version control system. This will let us deploy to heroku, a free Rails host. In fact, they have a quick introductory guide to using git, and links to additional git resources.

Once you have Rails and git set up, you’ll need to install the plist gem to your Rails project.

$ sudo gem install plist

Now we’re prepared to program the service.

Project Configuration

We’ll need to take a few actions to configure the service. First, we need to create a database schema to store our scores.

config/create_score_schema.rb

class CreateCoreSchema < ActiveRecord::Migration
  def self.up
    create_table :high_scores, :force => true do |t|
      t.string :player_name, :limit => 32, :null => false
      t.string :iphone_udid, :limit => 64, :null => false
      t.integer :total_score, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :high_scores
  end
end

config/routes.rb

ActionController::Routing::Routes.draw do |map|
  map.resources :high_scores, :only => [:index, :create]
end

config/environment.rb

RAILS_GEM_VERSION = '2.3.3' unless defined? RAILS_GEM_VERSION
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  config.gem 'plist'
  config.time_zone = 'UTC'

  module LeaderboardAuth
    SECRET_SALT = '999888999'
  end
end

Standard Rails configuration stuff so far, except note that we’re configing the plist gem here, and declaring a module. The purpose of LeaderboardAuth is just to reference a salt value, the same one used by the iPhone to hash the high score submission. It’s declared here in environment.rb for convenience and we’ll use it throughout the service and tests.

Leaderboard Web Service

Now that the configuration is set, we can start writing the service proper. We’ll only need a controller class, and a model class, since we won’t be serving any HTML, just plist XML lists of high scores.

First, our only model, HighScore.

app/models/high_score.rb

require 'digest'
require 'plist'

class HighScore < ActiveRecord::Base
    attr_accessor :auth_token

    validates_presence_of :player_name, :iphone_udid, :total_score, :auth_token
    validates_length_of :player_name, :within => 1..32, :allow_nil => true

    named_scope :sorted_by_score, :order => 'total_score DESC', :limit => 10

   def validate
      return if auth_token.blank?
      auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

      errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth
   end

   def to_plist_node
     return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)
    end
end

All pretty standard for a Rails model, and I’ll summarize: we need to require digest and plist so we can perform a MD5 hash, and serialize the model into plist format.

There isn’t an “auth_token” field in the database, but the model needs one to store the hashed token when the high score POST is submitted, so we add it as an attr_accessor.

The model will perform some basic validation, with the security test handled in the validate method. An MD5 hash is created from the model’s score, udid, and the application’s secret salt value. The format is the same as used on the iPhone, SCORE:SALT:UDID. Note, if you were running additional substitution routines you’ll have to reproduce them here as well while reconstructing the hash.

auth = Digest::MD5.hexdigest( [total_score, LeaderboardAuth::SECRET_SALT, iphone_udid].join(':') )

If the two hashes don’t match, we can assume that the submission has been altered.

errors.add(:auth_token, "unauthorized high score submission") if auth_token.downcase != auth

To serialize the HighScore model’s properties into plist format, we implement a to_plist_node method that the plist gem will use to format the output.

return {:player_name => self.player_name, :total_score=>self.total_score, :created_at => self.created_at}.to_plist(false)

The controller will only need to respond to GET and POST requests, to retrieve and submit high scores. It follows basic Rails conventions, and the real work is delegated to configuration settings and the high scores model.

app/models/high_scores_controller.rb

class HighScoresController < ApplicationController
    include ApplicationHelper

    skip_before_filter :verify_authenticity_token

    def index
      @scores = HighScore.sorted_by_score.all

      respond_to do |wants|
        wants.xml { render :xml => @scores.to_plist, :status => :ok }
        wants.any { render :nothing => true, :status => :ok }
      end
    end

    def create
      @highscore = HighScore.new(params[:high_score])

      header_status = @highscore.save ? :ok : :unprocessable_entity

      respond_to do |wants|
        wants.xml { render :nothing => true, :status => header_status }
        wants.any { render :nothing => true, :status => header_status }
      end
    end
end

And that’s the Rails online high scores service! To complete the project, we’ll add a few basic tests. I wouldn’t review them, but they should give us confidence to expand on the core leaderboard features without fear of breaking the service.

Testing the Service

We’ll need three files. A unit test, a functional test, and a fixture with a few mock scores.

test/fixtures/high_scores.xml

player1:
    player_name: player one
    total_score: 10
    iphone_udid: 2b6f0cc904d137be2e1730235f5664094b831186

player2:
    player_name: player two
    total_score: 15
    iphone_udid: 2b6f0ce614c43cbb2e0730235f5664094b83b487

player3:
    player_name: player three
    total_score: 20
    iphone_udid: 2bef0b11cff51c9c080730235f566401e80bfe1

test/unit/high_score_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')
require 'digest'

class HighScoreTest < ActiveRecord::TestCase
  fixtures :all

  def test_should_validate_auth_hash
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert score.save, "Save failed: #{score.errors.full_messages}"

  end

  def test_should_not_validate_with_bad_auth_hash
    salt = "xxxxxxxxxxxx"
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    score = HighScore.new( :player_name => name,
                          :iphone_udid => udid,
                          :total_score => 50,
                          :auth_token => auth_token)

    assert !score.save
    assert score.errors.on(:auth_token)
  end
end

test/functional/high_scores_controller_test.rb

require File.join(File.dirname(__FILE__), '/../test_helper')

class HighScoresControllerTest < ActionController::TestCase
  def setup
    @plist_expression = /<!DOCTYPE plist PUBLIC "-\/\/Apple Computer\/\/DTD PLIST 1\.0\/\/EN" "http:\/\/www\.apple\.com\/DTDs\/PropertyList-1\.0\.dtd">/
  end

  def test_should_get_index
    get :index, { :format => "xml", :limit => 10 }

    assert_response :success
    assert_not_nil assigns(:scores)
    assert_match @plist_expression, @response.body

    plist = Plist::parse_xml(@response.body)
    assert_equal "Array", plist.class.to_s
    assert_operator plist.size, ">", 0
  end

  def test_should_post_to_create
    salt = LeaderboardAuth::SECRET_SALT
    name = 'player X'
    udid = "2bef0b11cff51cbb2e0730235f5664094b83b487"

    auth_token = Digest::MD5.hexdigest( [name,salt,udid].join(':') )

    assert_difference('HighScore.count') do
      post :create, { :format => "xml", :high_score => {
                                          :player_name => name,
                                          :iphone_udid => udid,
                                          :total_score => 50,
                                          :auth_token => auth_token} }
    end

    assert_response :success
  end
end

Hosting the Service on Heroku

heroku is an excellent Rails host with fancy cloud architecture and a tiered pricing plan that starts at free. The best part about heroku, though, is how easy they’ve made Rails deployments. They did this by creating, essentially, a deployment gem that gives you some slick command line powers. Here’s how you deploy your app for the first time (ripped from the heroku front page) using the heroku gem, and of course, git.

$ sudo gem install heroku
$ heroku create myapp
$ git push heroku
$ heroku domains:add example.com
$ heroku rake db:migrate
$ heroku bundles:capture

You’ll get a basic subdomain at heroku.com which you’ll call from the iPhone. If your game takes off and traffic rises, heroku will scale seamlessly for you, and you’ll generally be well taken care of by a responsive support staff who know a great deal about the environment they host. Head over to their site for more information. (I’m in no way affiliated with them, just a happy user and impressed developer).


With the service built, and the iPhone posting and retrieving high scores, we’ve finally reached the end of the tutorial.

Good luck!

Managing Local High Scores and Online Leaderboard for your iPhone Games Part 1/2

Posted September 6th, 2009 in Uncategorized by Travis

Why high scores? Because the stamp of public achievement rewards players for investing their time and skill in your game; and because the one-upsmanship of ranking promotes a competitive social awareness which can make a title more enduring.

iPhone Leaderboard

With the body of iPhone gaming so often reminiscent of the arcade era, an online leaderboard seems natural to the platform and something that adds a breath of life to an otherwise perishable experience. In this two-part tutorial, I’ll explain how to store and load local and online high scores for a game using only the native iPhone libraries and a Rails web service.

This first part will cover modeling and storing local high scores, and in part two we’ll build the leaderboard web service.

Existing Frameworks

A number of iPhone frameworks for leaderboards, analytics, and social media have been appearing recently and certainly warrant investigation if you’re really looking to dress up your game with community features.

Scoreloop
http://corporate.scoreloop.com

A free service, “adding the Scoreloop SDK to your game gives your users access to features such as challenges, buddies, and high score lists.”

OpenFeint
http://www.openfeint.com

An extremely feature-rich and free service offering social challenges, offline support, in-app friending, high scores, and social media APIs.

iGetScores
http://www.igetscores.com

Free and open source (code.google.com/p/igetscores) “online high scores / ranking service which is convenient for developers and transparent to game players.”

Agon Online
http://developer.agon-online.com
“AGON Online is a complete social platform for iPhone and iPod Touch games. It is a location-aware online high score system, complete with profiles, friends, awards etc. Think Xbox LIVE on-the-go.”

Geocade
http://geocade.com/company.html
Also free, “Geocade is the largest location aware social gaming platform with gaming communities in thousands of cities and towns across the world.”

Requirements

Although each of those services is promising in its own right, we’re going to develop our own leaderboard server here for a few practical reasons.

  1. Avoid dependencies on external libraries
  2. Minimize codebase and build size by staying in the iPhone SDK
  3. Control encryption and avoid App Store export restrictions
  4. Build a system that does precisely what’s needed, no more or less
  5. Keep our user analytics and personal data 2nd party
  6. Learn by doing!!!

Before moving into the code, let’s take stock of the requirements for local high score support.

First, we’ll need a high score class that can be serialized for storage. We’ll need to be able to store and retrieve the records on the iPhone, and we’ll also need to display the records in a UITableView. Finally, we’ll need to process new high scores so that they’re ranked while all low scores are rejected.

Modeling the High Score Classes

HighScoreRecord.h

#import <Foundation/Foundation.h>

@interface HighScoreRecord : NSObject <NSCoding, NSCopying> {
	NSString *name;
	NSNumber *totalScore;

	NSDate *dateRecorded;
}

- (id) initWithScore:(NSString *)name TotalScore:(NSNumber *)totalScore;

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *totalScore;

@property (nonatomic, retain) NSDate *dateRecorded;
- (NSComparisonResult) compare:(id)other;
@end

Aside from the obvious properties, the HighScoreRecord class will need to implement NSCoding and NSCopyingfor serialization, and the NSComparisonResult method compare: so that it can be easily ranked against other HighScoreRecord instances.

HighScoreRecord.m

#import "HighScoreRecord.h"

@implementation HighScoreRecord

@synthesize name;
@synthesize totalScore;
@synthesize dateRecorded;

- (id) initWithScore:(NSString *)playerName TotalScore:(NSNumber *)score {
    if (self = [super init])
	{
		name = playerName;
		totalScore = score;

		dateRecorded = [NSDate date];
	}
    return self;
}

- (NSComparisonResult) compare:(id)other {
	return [self.totalScore compare:other];
}

#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)encoder {
	[encoder encodeObject:name forKey:@"Name"];
	[encoder encodeObject:totalScore forKey:@"TotalScore"];
	[encoder encodeObject:dateRecorded forKey:@"DateRecorded"];
}

- (id)initWithCoder:(NSCoder *)decoder {
	if(self = [super init]) {
		self.name = [decoder decodeObjectForKey:@"Name"];
		self.totalScore = [decoder decodeObjectForKey:@"TotalScore"];
		self.dateRecorded = [decoder decodeObjectForKey:@"DateRecorded"];
	}
	return self;
}

#pragma mark -
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone {
	HighScoreRecord *copy = [[[self class] allocWithZone:zone] init];
	name = [self.name copy];
	totalScore = [self.totalScore copy];
	dateRecorded = [self.dateRecorded copy];

	return copy;
}
#pragma mark -

- (void)dealloc {
	[name release];
	[totalScore release];
	[dateRecorded release];

    [super dealloc];
}

@end

We’ll also need a managing class that is responsible for storing and retrieving the scores, ranking them against each other, and, ultimately, communicating with the leaderboard server.

HighScores.h

#import <Foundation/Foundation.h>
#import "HighScoreRecord.h"

@interface HighScores : NSObject {}

+ (void)addNewHighScore:(HighScoreRecord *)score;
+ (void)saveLocalHighScores:(NSArray *)highScoreArray;

+ (NSString *)highScoresFilePath;
+ (NSMutableArray *)getLocalHighScores;
+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray;
@end

Look over the following implementation, and then we’ll walk through each of the methods in turn.

HighScores.m

#import "HighScores.h"

@implementation HighScores

const int HIGH_SCORE_COUNT = 10;

+ (void)addNewHighScore:(HighScoreRecord *)score {
	NSMutableArray *locals = [HighScores getLocalHighScores];

	int totalScore = [score.totalScore intValue];
	if (locals.count < HIGH_SCORE_COUNT){
		[locals addObject:score];
		NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
		[HighScores saveLocalHighScores:sortedLocals];
		[sortedLocals release];
	} else {
		NSUInteger lastIdx = HIGH_SCORE_COUNT-1;
		HighScoreRecord *lastRecord = [locals objectAtIndex:lastIdx];
		if (totalScore > [lastRecord.totalScore intValue]){
			[locals addObject:score];
			NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
			[sortedLocals removeLastObject];

			[HighScores saveLocalHighScores:sortedLocals];

			[sortedLocals release];
		}
	}
}

+ (NSString *)highScoresFilePath {
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:@"HighScoresFile"];
}

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}

+ (void)saveLocalHighScores:(NSArray *)highScoreArray {

	NSMutableData *data = [[NSMutableData alloc] init];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

	[archiver encodeObject:highScoreArray forKey:@"HighScores"];
	[archiver finishEncoding];

	[data writeToFile:[HighScores highScoresFilePath] atomically:YES];
	[archiver release];
	[data release];
}

+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray {

	NSString *SORT_KEY = @"totalScore";
	NSSortDescriptor *scoreDescriptor = [[[NSSortDescriptor alloc] initWithKey:SORT_KEY ascending:NO selector:@selector(compare:)] autorelease];
	NSArray *sortDescriptors = [NSArray arrayWithObjects:scoreDescriptor, nil];

	NSArray *sortedArray = [highScoreArray sortedArrayUsingDescriptors:sortDescriptors];

	return [[NSMutableArray alloc] initWithArray:sortedArray copyItems:NO];
}

@end

With our classes modeled, we can now examine how the storage and retrieval process actually works.

Storing the Data

In order to locally store our high scores, we’ll use the application’s NSDocumentDirectory, the recommended source for file management on iPhone apps. The records will simply be serialized to an NSArray of HighScoreRecord objects, which we can load and manipulate as needed.

Adding a score is called with addNewHighScore, which takes a HighScoreRecord and saves it if the score is high enough. To save a player’s score, have a method like so:

- (void)saveHighScore {
	int score = [somePlayerObject.score intValue];

	HighScoreRecord *highScore = [[HighScoreRecord alloc] initWithScore:name TotalScore:[NSNumber numberWithInt:score]];
	[HighScores addNewHighScore:highScore];

}

The addNewHighScore method is responsible for the controlling persistence logic, which includes a few simple rules. First, we should only store a maximum of HIGH_SCORE_COUNT records locally. Because of this constraint, only records that exceed the score of the last record in the collection will be saved; otherwise, they’re ignored. Once a new HighScoreRecord has been added to the high scores collection, it needs to be resorted and saved back to the file system.

The sortHighScoreDictionaryArray method is responsible for sorting the scores after a new record has been added to the collection. Using an NSSortDescriptor and the HighScoreRecord’s compare method, the scores are sorted on the totalScore property and a sorted array is returned.

Finally, saveLocalHighScores is called to persist the high scores collection. Its chief responsibility is serializing the HighScoreRecord objects to the NSDocumentDirectory.

Loading the Data

In the most common scenario you’ll be loading the high scores into a UITableView. The process is straightforward, amounting to deserializing the high scores file we’ve been saving.

The code in HighScoresViewController, which implements UITableViewDelegate and UITableViewDataSource, is quite standard. All the high score logic is handled by the other models, and populating a high scores display is simply a matter of making the right calls.

Note, since there won’t always be available local high score, cellForRowAtIndexPath first checks if a row exists and adds placeholder text if there aren’t enough scores to fill the table. In this case, we’ll set the UITableView to always show 10 high score slots with numberOfRowsInSection.

HighScoresViewController.h

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) loadLocalHighScores;

@end

HighScoresViewController.m

#import "HighScoresViewController.h"

@implementation HighScoresViewController

@synthesize scoreTableSelector;
@synthesize highScoreTable;
@synthesize highScoreData;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        [self loadLocalHighScores];
    }
    return self;
}

. . .

- (void)loadLocalHighScores {
	highScoreData = [[HighScores getLocalHighScores] retain];
}

. . . 

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	HighScoreCell *cell = (HighScoreCell *)[tableView dequeueReusableCellWithIdentifier: @"HighScoreCellIdentifier"];
	if (cell == nil) {
		NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"HighScoreCell" owner:self options:nil];
		cell = (HighScoreCell *)[nib objectAtIndex:0];
	}
	NSUInteger row = [indexPath row];
	cell.rankLabel.text = [NSString stringWithFormat:@"%d", row+1];

	if (row < highScoreData.count){
		HighScoreRecord *record = (HighScoreRecord *)[highScoreData objectAtIndex:row];

		cell.nameLabel.text = record.name;
		cell.scoreLabel.text = [NSString stringWithFormat:@"%@", record.totalScore];

		NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
		[dateFormat setDateFormat:@"yyyy-MM-dd"];

		cell.dateLabel.text = [dateFormat stringFromDate:record.dateRecorded];
	} else {
		cell.nameLabel.text = @"-";
		cell.scoreLabel.text = @"-";
		cell.dateLabel.text = @"-";
	}

	return cell;
}

. . . 

@end

You can note again that the responsibility for loading and deserializing the local high scores file rests with the HighScores class’s getLocalHighScores method. This is called by the HighScoresViewController in loadLocalHighScores.

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}


Part 2

In the second part of this tutorial, I walk through setting up a simple web service in Rails that will act as an online leaderboard, and we’ll apply some basic authentication to secure the high score submissions. And with git and heroku.com’s free Rails hosting, deploying the service will be about as frictionless as web development gets.

30 Review Sites to Promote Your iPhone Applications and Games

Posted August 24th, 2009 in Uncategorized by Travis

The internet is aflow with tears from developers bemoaning the oversaturation of the App Store. In truth, the App Store is now seeing the same saturation of other software markets. Developers are no longer finding themselves entitled to wild success on the basis of a swift release. Realities like promotion and strategy are taking hold, and if you want a lucrative place beside your competitors you’ll have to work just as hard at promoting your product as you always have.

Fortunately, the iPhone community is perpetually rapt with the buzz of new releases, a fact reflected in the innumerable review sites that have established themselves. I’ve compiled a list of some of these sites, along with statbrain‘s traffic estimates (which seem to error on the low side so far as I can tell) in order to help myself and other pureblood developers organize their promotional tactics.

148Apps (www.148apps.com)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here.
Visits/day: 8,328

App Chatter (www.appchatter.com)
Reviews iPhone and iPod touch applications and games, and provides a developer-managed profile for your releases. See their submissions guidelines here, and manage a profile from here.
Visits/day: 322

App Podcast, The (theapppodcast.com)
Reviews iPhone and iPod touch applications and games, but you must pay them for the privilege ($25-$100/review). You can find their submission guidelines here. You will need a video of your app in order to submit it for review and hosting on their video channels.
Visits/day: 3,773

App Safari (www.appsafari.com)
Reviews iPhone and iPod touch applications and games. See their submissions guidelines here.
Visits/day: 17,245

App Store Apps (www.appstoreapps.com)
Reviews iPhone and iPod touch applications and games. No guidelines, but contact them about a review here.
Visits/day: 7,187

AppCraver (www.appcraver.com)
Reviews iPhone and iPod touch applications and games, and also conducts developer interviews. See their submissions guidelines here.
Visits/day: 10,412

AppRater (www.apprater.com)
Lists and “reviews” iPhone applications, but requires link sharing from participating developers. Details here.
Visits/day: 7,269

AppShopper (appshopper.com)
Reviews iPhone and iPod touch applications and games, and doesn’t openly list their submission guidelines. You can try contacting them at arn@appshopper.com
Visits/day: 27,479

Apptism (www.apptism.com)
Reviews iPhone and iPod touch applications and games, including previewing those that haven’t yet been released. See their submissions guidelines here.
Visits/day: 19,121

AppVee (www.appvee.com)
Gives *video* reviews of iPhone and iPod touch applications and games. See their submissions guidelines here.
Visits/day: 7,379

Apple iPhone Apps (appadvice.com/appnn)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here.
Visits/day: 9,890

Apple iPhone School (www.appleiphoneschool.com)
Reviews iPhone and iPod touch applications and games from both the App Store and Cydia. No public submission guidelines, but you can contact them here.
Visits/day: 17,342

Art of the iPhone (artoftheiphone.com)
Reviews iPhone and iPod touch applications and games. No public submission guidelines but you can contact them here.
Visits/day: 5,290

iPhone Application List (iphoneapplicationlist.com)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here, although they do ask for a reciprocal link in exchange for a review.
Visits/day: 9,867

iPhone App Review (www.iphoneappreview.com)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here.
Visits/day: 3,959

iPhone App Reviews (www.iphoneappreviews.net)
Reviews iPhone and iPod touch applications and games. The site allows you to publish promo codes with your review as well. You can find their submission guidelines here.
Visits/day: 5,145

iPhone Game Network (www.iphonegamenetwork.com)
Reviews iPhone games, including Ad-Hoc distributions. You can find their submission guidelines here.
Visits/day: 4,819

iPhone Game Reviews (iphone-game-reviews.com)
Small review blog for iPhone games, you can find their submission guidelines here.
Visits/day: 469

iPhone Games (iphonegames.com/reviews)
Small review blog for iPhone games, you can find their submission guidelines here.
Visits/day: 538

iPhone Footprint (www.iphonefootprint.com)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here, and request a review here.
Visits/day: 7,865

iPhone World (www.iphoneworld.ca)
Reviews iPhone and iPod touch applications and games, as well as publishes press releases. You can contact them about app submissions here.
Visits/day: 10,318

NativeiPhoneApps (www.nativeiphoneapps.com)
Reviews iPhone and iPod touch applications and games. You can find their submission guidelines here.
Visits/day: 761

Slide to Play (www.slidetoplay.com)
Reviews iPhone and iPod touch games. You can find their submission guidelines here.
Visits/day: 8,882

App Theater (www.apptheater.com)
Reviews iPhone and iPod touch applications and games. You can find their contact details here.
Visits/day: 970

Touch Arcade (toucharcade.com)
Excellent review site for iPhone and iPod touch games. You can find their submission guidelines and other useful information here.
Visits/day: 44,474

MoDoJo (www.modojo.com)
Promtional site for mobile games allowing you to submit reviews, maintain a profile, and so forth. You can find details here.
Visits/day: 4,823

Touch Reviews (touchreviews.net)
Reviews iPhone and iPod touch applications and games, as well as several additional promotional tools. You can find more details here.
Visits/day: 2,183

Buy me an iPhone (www.buymeaniphone.com)
Reviews iPhone and iPod touch applications and games. You can find their contact details here.
Visits/day: 18

What’s on iPhone (www.whatsoniphone.com)
News and reviews for the iPhone. You can find their contact details and submission information here.
Visits/day: 6,763

Pocket Gamer (www.pocketgamer.co.uk)
Reviews games on various mobile platforms including the iPhone. See their submission guidelines here and check out the iPhone developer forums here.
Visits/day: 3,017

iPwn Games (www.ipwngames.com)
Reviews iPhone and iPod touch games. You can find their submission guidelines here. They also offer personal blogs to game developers here.
Visits/day: 5,344


For some general review tips you’ll find an instructive post over at mobile orchard: http://www.mobileorchard.com/five-tips-for-getting-iphone-reviews/

UPDATE: Additional sites recommended by friendly internet commenters:

APPera, The (www.theappera.com)
Reviews iPhone and iPod games. You can find their submission guidelines here.
Visits/day: 3,719

AppGamer.net (www.appgamer.net)
Reviews iPhone and iPod games. You can find their contact details here.
Visits/day: 979

Touch my Apps (www.touchmyapps.com)
Reviews iPhone and iPod applications and games. They also do interviews with artists, developers, etc. You can contact them here.
Visits/day: 2,262

Portable Gamer, The (www.theportablegamer.com)
Reviews portable games, including iPhone games. You can find their contact details here, and a their iPhone game submission form here.
Visits/day: 2,269

appVersity (www.appversity.com)
Reviews iPhone and iPod applications and games. You can find their submission guidelines here.
Visits/day: 2,605

AppModo (www.appmodo.com)
Reviews mobile applications and games, including the iPhone. They also do interviews with artists, developers, etc. You can find their contact details here.
Visits/day: 6,394

iPhone: Responding to UITabBarController Touches

Posted August 14th, 2009 in Uncategorized by Travis

Responding to the iPhone’s UITabBarController touches should be a direct, uncomplicated process, but in fact requires you to establish a number of hooks throughout your code. There are many reasons you might wish to intercept a UITabBar action, either to cancel and return to a prior tab, to show an alert view, to conditionally push a stack of ViewControllers , and so forth. Here are the steps necessary to make that happen.

(1) Add a UITabBarController property to your App Delegate’s class so you can access it through Interface Builder.

@interface MyAppDelegate : NSObject <UIApplicationDelegate>
{
   UITabBarController *tabBarController;
}
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;

(2) Use Interface Builder to associate the tabbar control with the app delegate’s tabBarController property.

(3) Add the necessary delegate methods to your app delegate. UITabBarControllerDelegate is required, but if you want to use a UIAlertView as in this example, you’ll also need to add UIAlertViewDelegate as well.

@interface MyAppDelegate : NSObject <UIApplicationDelegate, UITabBarControllerDelegate, UIAlertViewDelegate>

(4) Override the tabBarController’s didSelectViewController method. You can use the tabbar’s selectedIndex property to determine which tab was touched; in this example, an alert view confirms whether or not the user wants to quit the app.

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {

   NSLog(@”User selected %@ from the tabbar”,viewController);

   if (tabBarController.selectedIndex == 3){
      UIAlertView *actionAlert = [[UIAlertView alloc]
      initWithTitle:@”Quit Application”
      message:@”Are you sure you want to quit the application?”
      delegate:self cancelButtonTitle:nil
      otherButtonTitles:@”Quit”, @”Resume”, nil];
      [actionAlert show];
   }
}

(5) Finally, respond to the alert view depending on the user’s selection.

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
   if (buttonIndex == 0) { // Quit
      [self performSelector:@selector(saveAppAndExit:) withObject:nil afterDelay:.1];
   }
   else if (buttonIndex == 1) { // Resume
      tabBarController.selectedIndex = 0; // Default tab
   }

   [alertView release];
}

There you have it; like many such tasks on the iPhone, it’s a simple problem otherwise obscured by the platform’s particular idioms. Note, there is one caveat: by the time you gain programmatic access to the UITabBarController via didSelectViewController, the selection has already occurred such that you can’t intercept the action, only respond to it.

iPhone Guidelines and Resources for Artists

Posted July 28th, 2009 in Uncategorized by Travis

The Basics

Screen Resolution: 320×480 (or 480×320 landscape mode)
Colors: 262,144
Resolution: 160 dpi
Contrast: use higher contrast than normal for art on the iPhone screen
Preferred Graphics Format: .PNG

Fairlady Media has a great instructional post covering a variety of topics about creating iPhone graphics: http://fairladymedia.com/node/10

Application Logo and Icon

In order to distribute an application in the App Store, Apple requires two main  graphics:

(1) A large 512×512 logo, and;

(2) a smaller 57×57 version of the same.

The small logo must resemble the large logo, although there can be minor differences in perspective and detail, or the application may be rejected for inconsistent logo design. The 512×512 logo is what people see when they are browsing iTunes, while the 57×57 logo will be the application’s icon on the iPhone itself.

The App Store and the iPhone will apply additional effects to the app logos by rounding the corners and applying a light gloss. One of the tasks when designing a logo is to ensure that it will look good with the iPhone effects applied.

The Tabbar

The tabbar is a native UI element displayed at the bottom of the screen. Each tab button may contain text and an icon. The following guidelines apply for tabbar icons:

  • Use the PNG format.
  • Sized 30×30 (roughly – you have a little leeway, ~25-35)
  • Use pure white with appropriate alpha
  • Do not include a drop shadow.
  • Use anti-aliasing.

There’s a painstakingly informative tutorial at Osmorphis about the process of creating tabbar icons (summary here), but the thing that deserves special attention is that the icons must be drawn in B&W alpha, and in negative. The Osmorphis tutorial covers the steps on how do this pretty efficiently in Photoshop.

Layout, Templates, Mockups

The following PSDs contain templates and native iPhone UI elements that you can use as building blocks for your mock ups.

iPhone GUI PSD
http://www.teehanlax.com/blog/?p=1628

iPhone Kit
http://andrewbadger.deviantart.com/art/iPhone-Kit-108089727

Resizable iPhone Shell PSD
http://twotoasters.com/index.php/2009/06/02/resizable-iphone-psd/

Fonts

These are the standard iPhone system fonts available. It’s much harder than it should be to use a custom font on the iPhone.

American Typewriter, AppleGothic, Arial, Arial Rounded MT Bold, Arial Unicode MS, Courier, Courier New, DB LCD Temp, Georgia, Helvetica, Helvetica Neue, Hiragino Kaku Gothic ProN W3, Hiragino Kaku Gothic ProN W6, Marker Felt, STHeiti J, STHeiti K, STHeiti SC, STHeiti TC, Times New Roman, Trebuchet MS, Verdana, or Zapfino.

Colors: 262,144