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
2009 September | Travis Dunn

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/)

Programming at Full Speed: Accelerating the Central Nervous System, Reducing Brain Resistance

Posted September 24th, 2009 in Uncategorized by Travis

The winner of the Netflix million dollar prize contest was announced this month. The two finishers, BellKor and The Ensemble, submitted their final entries 20 and 4 minutes before the deadline, respectively, securing BellKor the win by a harrowing 16 minute difference. I can only imagine the feverish pandemonium that must have erupted from both teams as the appointed hour approached. They must have been hurridly iterating to the very end, and whether ultimately through personal heroism or resourceful team support, the victory blow was delivered by programmers who have no doubt brought intensity and speed into their skill sets as a decisive advantage.

It’s that speed which can be worth a million dollars, or at least perhaps, seek and destroy eleventh hour bugs, ship a feature that’s been dropped for time, or make weekend projects feasible. High level productivity gains come from engineering and design decisions and are sustained through appropriate project management. The subject has earned the thoughtful analysis of countless technologists and programmers, but it usually stops short of the visceral fact that the sheer intensity of relentless programming can – in brute, anarchic glory – make more of an impact at the end of the day than any defensive design and planning. Code must be written, and no architectural optimizations can abstract that away.

I don’t want to dovetail into a blissfully approving flight path of crunch times or task-mandates that doing it right now is more important than doing it right. But that’s no reason not to celebrate a corner skill in our profession which everyone seems to possess at a demonstrably different level. And that’s certainly no reason to avoid overexerting ourselves in exercise, given that once we’ve grown accustomed to a hurried pace it becomes easier to apply in everyday affairs, to enter rush mode at will, to let the mind drift to planning while the fingers type the work.

So what are some ways to become a faster programmer?

Programming Competitions

More of a strategy than a tactic, willfully committing yourself to short, intense programming sessions over the weekend where you’re galvanized by the thrill of competition and the taste of an immediate product has got to be one of the best (albeit unscientific) ways to improve your programming speed. It’s not so much about learning tricks or practice, but personally acquainting yourself with a philosophy of frantic, obsessive concentration on publishing a small deliverable.

The scope is about right for these competitions to allow for focused programming with little worry of feature creep, reevaluations of design as complexities are uncovered, or the many other thoughtful distractions which constitute the larger role of development.

Words per Minute

The core virtue of fast programmers is lightning typing speed. You can’t peck aloofly at the keyboard, but must violently hammer away as if zombies were attacking your office and your life depended on completing a block of code before the barricades break. Youthful vigor counts for a lot, but so does unrelenting discipline.

Snippets and Generators

Most IDEs now have tools for managing code snippets and/or powerful template generators. Identify the handful of code you’re repeating most often – say, accessor methods – and write a snippet for it. I’ve only found myself writing perhaps 2-5 snippets at most, almost always to make up for shortcomings in the syntax and idioms of the platform I’m working on.

Hotkeys and Text Expansion

Similarly, dedicate yourself to learning the hotkeys of the environments you’re working with; print up cheat sheets and forbid yourself from using the mouse to perform actions with a known hotkey. Embrace mouseless computing.

Text expansion/intellisense/code completion is a special subset of productivity located in the mouseless computing paradigm, and the quicker you can type through heavily parameterized function calls the further you’ve risen above the limitations of verbose languages, libraries, APIs.

Head Programming

The quickest way to program is to embark with firm goals and implementation plans, programming the project in your head before you ever touch the keyboard. Formal specs take too long and impulsive programming leads to wasteful back-stepping, so the idea is to develop a balanced mental tour that gives you just enough context and boundaries so that, at any given point as you code, you’ll know what precisely what the next step should be.

Sub-Machine Guns

Prolonged white-knuckled programming is an unsustainable weapon in the developer’s arsenal, and must be used properly. By properly I mean “like a sub-machine gun”: sustained fire in short, controlled bursts. With ass on seat and fingers on the keys, dump as much code as possible onto the screen, but stop periodically to cool down and re-aim. The pause is only to gather your resolve for another assault and it mustn’t last long, merely long enough to ensure that you’re hitting your target and to crack your knuckles.

Mock Code

Building and propelling momentum is at the heart of fast programming (and, for that matter, entering the fabled “zone”). Anything that kills this momentum, even as a necessary evil, should be avoided. If an implementation detail is threatening to arrest your speed, abstract the offending code and push if off into some stub to be revisited later, preferably while waiting for a compile or test suite to run.

Working code is paramount when you’re racing the clock, and if you can defer some unexpected analysis until later, so much the better. My assumption is that a mocked feature now with full supporting code is better than uniformly functional but incomplete code. Yes, even magic numbers are better than putting on the brakes.

Code on the Side

Every second is precious, and we regularly endure brief moments of inopportune downtime while we compile the code, deploy it, launch the environment, run tests, transfer files, and so on. Always keep some code on the side to jump to and from during these lags. It must, of course, be the sort of code amenable to quick modifications, and requiring only minimal effort to mentally reorientate to and resume. There are always edges of a project like this, however, and the trick is strategically saving them to fill in otherwise unproductive gaps.

Improving Tag Ownership in acts_as_taggable_on

Posted September 18th, 2009 in Uncategorized by Travis

One of the features which sets acts_as_taggable_on apart from other Rails tagging plugins is the acts_as_tagger mechanic of tag ownership, whereby a “tagger” model tracks its tagging of other models. This design unlocks a lot of power in managing dynamic, abstract relationships.

Unfortunately, it happens to be one of the less mature areas of the code and there isn’t much in the way of convenience methods to help us traverse the tagger relationship. Finally, I uncovered an insidious bug in the tagging conditions that will reject valid owner tagging, and which must be fixed if you wish to use the acts_as_tagger parts of the plugin.

In order to extend acts_as_taggable_on, we’ll need to add a Ruby file defining our extensions to the plugin classes. We’ll also use this file to patch the tag condition bug by overriding the problematic method, save_tags. Because we should be able to continue updating the plugin without overwriting our changes, we’ll keep this in the Rails catchall code directory, /lib, and initialize it from our config/initializers directory.

Let’s start by adding two additional files to the project:

  • /config/initializers/plugin_extensions.rb
  • /lib/plugin_extensions/acts_as_taggable_on.rb

The plugin_extensions.rb file is quite simple, serving only to apply our changes when the app loads.

plugin_extensions.rb

require 'plugin_extensions/acts_as_taggable_on'

To the acts_as_taggable and acts_as_tagger modules, we’ll be adding two finder methods: find_taggers_for and find_tagged_by. These will return, respectively, a collection of taggers who have tagged a given taggable object, and a collection of taggable objects tagged by a given tagger. Nothing elaborate, just a couple of methods to improve the acts_as_tagger implementation. Here’s what the skeleton code looks like.

acts_as_taggable_on.rb

module ActsAsTaggableOnFindTaggersExtension
    . . .
end

module ActsAsTaggableOnFindTaggedByExtension
    . . .
end

module ActiveRecord
  module Acts
    module Tagger
      module SingletonMethods
        include ActsAsTaggableOnFindTaggersExtension
      end
    end
  end
end

module ActiveRecord
  module Acts
    module TaggableOn
      module InstanceMethods
        def save_tags
          . . .
        end
      end
      module SingletonMethods
        include ActsAsTaggableOnFindTaggedByExtension
      end
    end
  end
end

This code tells the original acts_as_taggable_on to include our extension modules, as well implementing an override of the save_tags InstanceMethod in order to fix the tag condition bug I’ve mentioned. Let’s fix that first. Here’s the original save method:

def save_tags
  (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
    next unless instance_variable_get("@#{tag_type.singularize}_list")
    owner = instance_variable_get("@#{tag_type.singularize}_list").owner
    new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
    old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }

    self.class.transaction do
      base_tags.delete(*old_tags) if old_tags.any?
      new_tag_names.each do |new_tag_name|
        new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
        Tagging.create(:tag_id => new_tag.id, :context => tag_type,
                       :taggable => self, :tagger => owner)
      end
    end
  end

The problem occurs on this line:

new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)

When differencing the old tag names with the tags_on method, the code will reject any matching tags in order to prevent duplicate tagging. However, duplicate taggings are perfectly valid when tracking taggers; as written, only the first tagger to tag a model will have their tag saved. Thankfully, the solution is simple: the tags_on methods takes another optional parameter, owner. If a tagger model is provided, tags_on will correctly filter the tag collection.

All we must do, then, is supply the tagger object here, and the code will difference the tags as expected. We’ll finish filling out this portion of our extension file with the overridden save_tags method containing the fix.

module ActiveRecord
  module Acts
    module TaggableOn
      module InstanceMethods
        def save_tags
          (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
            next unless instance_variable_get("@#{tag_type.singularize}_list")
            owner = instance_variable_get("@#{tag_type.singularize}_list").owner
            new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type, owner).map(&:name)
            old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }

            self.class.transaction do
              base_tags.delete(*old_tags) if old_tags.any?
              new_tag_names.each do |new_tag_name|
                new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
                Tagging.create(:tag_id => new_tag.id, :context => tag_type,
                               :taggable => self, :tagger => owner)
              end
            end
          end

          true
        end
      end
      module SingletonMethods
        include ActsAsTaggableOnFindTaggedByExtension
      end
    end
  end
end

The two custom finder methods for tagger models will mimic the existing find_tagged_with and find_options_for_find_tagged_with methods. Behind the finder methods, the real work is done by options methods which compose the necessary raw SQL, ActiveRecord conditions, and so forth. This is where any other filtering options would be processed if you wanted to further customize the tag retrieval logic.

module ActsAsTaggableOnFindTaggersExtension
    def find_taggers_for(*args)
      options = find_options_for_find_taggers_for(*args)
      options.blank? ? [] : find(:all,options)
    end

    def find_options_for_find_taggers_for(tags, taggable, options = {})
      tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)

      return {} if tags.empty?

      conditions = []
      conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]

      unless (on = options.delete(:on)).nil?
        conditions << sanitize_sql(["context = ?",on.to_s])
      end

      taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"

      conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")

      conditions << sanitize_sql(["#{taggings_alias}.taggable_id = #{taggable.id} AND #{taggings_alias}.taggable_type = '#{taggable.class.to_s}'"])

      { :select => "DISTINCT #{table_name}.*",
        :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.tagger_id = #{table_name}.#{primary_key} AND #{taggings_alias}.tagger_type = #{quote_value(base_class.name)} " +
                  "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
        :conditions => conditions.join(" AND ")
      }.update(options)
    end
end

module ActsAsTaggableOnFindTaggedByExtension
    def find_tagged_by(*args)
      options = find_options_for_find_tagged_by(*args)
      options.blank? ? [] : find(:all,options)
    end

    def find_options_for_find_tagged_by(tags, tagger, options = {})
      tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)

      return {} if tags.empty?

      conditions = []
      conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]

      unless (on = options.delete(:on)).nil?
        conditions << sanitize_sql(["context = ?",on.to_s])
      end

      taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"

      conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")

      conditions << sanitize_sql(["#{taggings_alias}.tagger_id = #{tagger.id} AND #{taggings_alias}.tagger_type = '#{tagger.class.to_s}'"])

      { :select => "DISTINCT #{table_name}.*",
        :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
                  "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
        :conditions => conditions.join(" AND ")
      }.update(options)
    end
end

Once we’ve defined these methods and included the modules in acts_as_taggable_on, we can invoke them to easily return a list of taggers, or a tagger’s tagged models.

@some_user.tag(@some_photo, :with => "paris, normandy, europe", :on => :locations)
@another_user.tag(@some_photo, :with => "europe", :on => :locations)
@another_user.tag(@another_photo, :with => "europe", :on => :locations)
Photo.find_taggers_for(' europe', @some_photo, :on => :locations)
User. find_tagged_by(' europe', @another_user, :on => :locations)

9 Ways Developers Fail to Accommodate Business

Posted September 15th, 2009 in Uncategorized by Travis

Developers tend to advertise themselves, indeed, their entire profession, as a pragmatic yet sophisticated participant in business, creating real human value while neatly abstracting away the complexities of technology. They often see themselves in opposition to other participants, engaging in heroics, or protectionism, or just toiling unsung for the higher good.

It’s a flattering portrait that manages to combine superiority, rationality, and integrity in the developer’s character, appearing all the more inviolable when compared to abusive, thoughtless management or the gasping demands of the customer.

The Lawnmower Man was an irresponsible developer

These are gross straw-men, however. Even if, as developers, the relationship between business and technology, between corporate and creative, places us under the thumb of clueless decision makers, and even if those authorities are wielding capricious power that can entangle and maim a project, we still have much to answer for ourselves, nor are we as martyred as we like to believe.

If management abuses power, then developers abuse knowledge. It’s around this fact where developers most often fail to support business needs and the “outsiders” who control them. We act as the privileged gatekeepers for warrens of technological complexity, and it’s our responsibility to examine, understand, distill, and communicate the features and tradeoffs of these systems. When we hoard knowledge, saturate our colleagues with irrelevant details, or refuse to take responsibility outside of our technological boundaries, we fail business.

And it’s not always a failure of omission or laziness, because by deliberately hiding or revealing technical facts, developers have the power to steer business decisions and often use this power to a greater or lesser degree in fighting back against perceived corporate hostilities or mistakes. Such influence may be well-meaning or even necessary, but often it’s the result of tensions appearing in a project or a developer who miscalculates their role.

With that said, here are some of the ways that developers fail their businesses.

1. Justification Fail: a failure to rationalize our technical decisions to others and adequately explain our decision making process. As much as software development is about managing tradeoffs, it’s important that we remain aware of the reasoning behind founding technical decisions we make, and likewise important to be able to account for our rationale to laymen.

2. Dependency Fail: a failure to understand, lucidly explain, or else explain at the right moment the deep dependencies between technical components, and the ramifications these relationships impose on system design or change requests. It’s the absolute responsibility of the developer to inform others when outside influence is threatening a dependency such that subsidiary work will be required or other unintended consequences may occur.

This is one of the most serious fails a developer can make, since we’re normally the only individuals with a clear understanding of a system’s composition and behaviors and the ways these drive each other. We’re also a vanguard, expected to remain vigilant to dependency problems that emerge in the course of development, and only we have the power to alert the team to forthcoming dependency tension or else fail to do so.

3. Transparency Fail: Due to the arcane nature of development work, it’s easy for our habitual work efforts to become unintelligible, maybe even invisible, to outsiders. It’s our failure that we don’t make some effort to communicate our activities at a high level, so that colleagues feel everyone is moving forward together.

4. Scheduling Fail: a common failure, partly due to the nature of software development, but also because developers are prone to confusing known and unknown elements, or mistaking the later for the former. When estimating development schedules, we ought to provide solid estimates for tasks of known scope while allowing for sufficient leeway in undertaking tasks of unknown scope.

5. Knowledge Transfer Fail: accumulating vast reserves of formal and informal intelligence about a system, developers often fail to share that knowledge, and become human silos for some of the most core competencies of a company. A developer should be to some extent documenting their design and decision making process in respect to current development work AND to how it may impact future development. Transferring knowledge means allowing other developers to quickly adopt the insights and proceedings you’ve recorded, while failing to do so means making yourself both an essential resource but also an extreme liability.

6. Platform Fail: a failure to select a platform or implementation for ANY reasons other than it’s inappropriateness to the project on hand and direct development productivity. Playing favorites with technologies is a selfish and chauvinistic fail, and developers ought to be expected to learn new platforms when it makes sense to do so, and to evaluate them so they recognize that sensibility in the first place.

7. Security Fail: while security is often overlooked by all sectors of business, a developer should always be passively aware of vulnerabilities in the systems they command, and go out of their way to ensure that even if these aren’t addressed that the risks and consequences are understood by everyone accountable. Only a developer can evaluate this accurately, so they must be a vocal advocate of security (or lack thereof) awareness.

8. Priority Fail: failing to prioritize workloads based on productivity, business requirements, or known scope is inimical to a project. Approaching development on the basis of personal or professional interest undermines the already fragile development cycle, and is by contrast a demotivating factor when it comes time for the priority work to be tackled.

9. Social Fail: there are plenty of fail-heavy stereotypes about developer personalities, and they aren’t worth rehearsing because by social failure I don’t mean likability, but rather failing to integrate with a team, as if you yourself were some closed social format and the rest of your office was talking in open source protocols.

I wouldn’t call unfriendliness or cynicism a fail, but not sharing your insights and teaching things to your team, not advocating for best practices or introducing ground-up improvement in your team’s spoken or unspoken development methodology, refusing to support incidentally related issues with your expertise, neglecting all efforts to advocate richer and more interesting possibilities for technology, and proudly shunning compromise during interpersonal conflict, these all constitute grounds for social failure where a developer, a considerable repository of knowledge and opinion, neglects to participate in their community or decides that other humans must implement implement their jobs to the developer’s “proprietary, social specifications”.


However true it is that development is often scapegoated, mistreated, distrusted, overruled, or abused by Corporate Masters, this shouldn’t be cause to retreat from a wider prospective or shut ourselves off to the subtle or reactionary or purely unintentional ways we fail to contribute larger project goals. It should never be an excuse for our own incompetency or negligence, and I think the developer community would be much better served by relaxing some of its criticism of other vocations and tightening the criticism of its own habits.

Software projects rarely fail for technical reasons, but developers tend to accept this as evidence that they have no done wrong, when they are just as human and answerable as anyway else involved. Our value as developers derives heavily from our wisdom, insight, and expertise. When we miscalculate the importance of that, or selectively use it to exert control, we are failing the businesses, and people, that we are supposed to help.

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]=%@&amp;high_score[player_name]=%@&amp;high_score[total_score]=%@&amp;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:&amp;format errorDescription:&amp;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!

Bug combining acts_as_taggable_on tagged_with scopes

Posted September 8th, 2009 in Uncategorized by Travis

My latest Sogeo work is getting quite a lot of distance out of tagging. With mbleigh’s acts_as_taggable_on I’ve been able to conveniently normalize several different data dimensions down into a tag model.

Modeling this was easy enough, but today I ran into an issue when constructing some of the more sophisticated filtering queries.

acts_as_taggable_on utilizes named scope to provide helpful finder methods, like so:

Location.tagged_with("mytag", :on => :tags)

Easy enough to combine with existing scopes:

Location.include_details.sorted.tagged_with("mytag", :on => :tags)

The problem arises when combining two tagged_with scopes, for example:

Location.tagged_with("mytag", :on => :tags) .tagged_with("mycategory", :on => :categories)

You can read a brief discussion of the problems over on this lighthouse ticket, but the crux of the issue is malformed generated SQL.

Until acts_as_taggable_on is updated, a simple if dreadfully inefficient solution is to call the collection for each tagged_with filter, and then combine the results.

location_scope = Location.include_details.sorted
tag_scope = location_scope. tagged_with("mytag", :on => :tags)
category_scope = location_scope. tagged_with("mycategory", :on => :categories)
category_scope.all(conditions) & tag_scope.all(conditions)

This is a serviceable enough solution for me for now, but if this is still a problem in a few months when my app hits production then I’ll have to start working on a real fix for combining multiple tagged_with helper scopes.

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.