Categories
Uncategorized

Every Marathon Marathon Calendar iPhone App on App Store

 

My new app, the Every Marathon Marathon Calendar, just got approved by Apple, and is available on the App Store!

This app came about because I’ve been planning my travel around running marathons on road and trail in all fifty states, and I’ve gotten obsessive about finding all of the most obscure marathons in the US. I kept on going to several different sites from my iPhone, and getting frustrated by the user interface. I’ve been working on my active city guide web site in Drupal, and I’d added about a hundred marathons there already….one thing led to another and I had over 400 marathons!

I certainly haven’t run every marathon in the country, but I’ve finished fifty-one marathons or ultramarathons, including the Race to the Altar Trail Marathon, which I co-race directed! A long way from when I couldn’t even run around the block in 2005!

Categories
Drupal Planet iPhone Development Ruby

Content Authoring for an iPhone App with Drupal using MongoDB

I just finished an interesting project – the EveryMarathon marathon calendar iPhone app. I’m working on my Active City Guides – and as part of that project, I ended up finding every American marathon. Running marathons is one of my hobbies, as is travel, and I like to combine the two by planning trips around marathons. There are several web sites with marathon calendars, but no iPhone (or Android) apps that I could find. So why not make one of my own? I could have put a mobile theme on my Drupal site, but that’s too easy 🙂

One of the screens from the app that lets you track your favorite marathons and your finished marathons

Content Storage and Import

Because I already had the content in Drupal 6, that simplified some of my decisions. In addition, the races themselves would be read-only on the app, meaning I wouldn’t have to worry about editing them and syncing them back to the server. I’d have to get the content from Drupal into the iPhone app and store it – there are several choices I could make:

Core Data

Use Core Data with a SQLite database and import Race objects (and their dependencies) into the database. Sync the database with Drupal on load or in the background, using JSON and a library like Lidenbrock.

Plists from Drupal 6 over HTTP with Services

Use Drupal iOS SDK with Services 2.2 and the Plist Server Module. Supports Views, Nodes, Comments, Users, and Taxonomies. Most Drupal-centric solution

JSON from Drupal 6 over HTTP with Caching

Use Drupal’s REST Server with JSON for Views and Nodes. Use an iPhone REST library such as  RestKit.

JSON from Drupal 6 bundled with App

Pull JSON out of Drupal 6 with REST Server Nodes, and bundle it in with the iPhone app. Read JSON off of the file system when needed. Has the huge advantage of not needing sync code right away, as updates to content can be shipped with app releases – my content isn’t particularly timely, as the marathon dates get updated once a year. Also quite fast to load JSON directly into arrays or dictionaries in the iPhone app and then display them in UITableViews.

I went with the bundled JSON approach for my first release of the app. One of the great things about knowing there are different approaches is that you can make sure your app is flexible enough to change approaches in a new version if you need to – I’ve got everything abstracted away to a singleton EveryMarathonData model object that gets races for iPhone view controllers.

Using MongoDB to aggregate content for the App

Part of the app’s functionality is to display races by state and by month:

I wanted to keep the mobile experience simple – no Advanced Search features, no complicated sorting. I could certainly use Objective-C on the iPhone to display sorted arrays for me – the Races by State could be calculated on startup with the number of races each state has, the Races by Month could be sorted out from all of the races, but these will always be the same for a set of content. I wanted to have a very fast startup time and also have a laggy, crashing iPhone app – so I went with simple. I used MongoDB, a NoSQL database that works very well with JSON, to create JSON documents that had exactly the information I needed to display, in the order they would be shown. This turned out to be simple. I have a two-step process:

1. Download JSON nodes from Drupal and store them in MongoDB
2. Create JSON to bundle with the iPhone app from MongoDB

I wrote my MongoDB integration code in Ruby, but any language would have worked. Here’s my ruby script that generates the JSON for the iPhone app screen above:

require 'rubygems'
require 'json'
require 'mongo'

states = Array.new

db = Mongo::Connection.new.db("marathondb")
coll = db.collection("nodeData")

state_abbr = {
  'AL' => 'Alabama',
  'AK' => 'Alaska',
 ...other states omitted...
  'WI' => 'Wisconsin',
  'WY' => 'Wyoming'
}

reduce ="function(key, values) { " +
                    "var sum = 0; " +
                    "values.forEach(function(f) { " +
                    "   sum += f.count; " +
                    "}); " +
                    "return {count: sum};" +
                "};"

map = "function() { emit(this.field_location[0].province, {count: 1}); }"
races_by_state = coll.map_reduce(map, reduce,{'query'=> {"field_race_types.value" => "Marathon"}})
races_by_state.find().to_a.each do |f|
	state = Hash.new
	state[:state] = "#{f['_id']}"
	state[:count] = "#{f['value']['count']}"
	state[:name] = state_abbr[state[:state]]
	states.push state
end

File.open("races_by_state.json","w") { |f|
		f.puts JSON.pretty_generate states
	}

Basically, we make a connection to the MongoDB for a database and a collection of documents, then create an array of state names and abbreviations. MongoDB doesn’t use SQL, but we can use Map/Reduce to simulate a simple Group By SQL query.

We then prepare our JSON the way we want to use it in the iPhone app – an array of Dictionary/Hash objects. Each Hash object has the state’s abbreviation, the number of races for the state, and the full name of the state

The easiest part of the whole script is outputting the JSON to the file system – the json Ruby gem does all of the hard work!

Wrap Up

After running my ruby scripts, the app data lives in a “magic” directory that XCode knows to bundle into the app. From Objective-C, I can load any of these JSON files into memory, and then parse them into NSDictionary and NSArray objects.

Categories
iPhone Development

Three20: Change Background of Grouped Table View

After going through the first beta testing round for my EveryMarathon iPhone application, several of my friends mentioned that the race details screen was a little…boring. I had the standard UITableView with a grouped style, default background, default controls – pretty dull.

One of the easiest ways to brand an iPhone app is to change the background of a grouped table view to something a little more interesting – a pattern, or a 320×480 image. You end up losing the ability to use a regular UITableViewController – instead you have to use a UIViewController with a UITableView, and then set up the datasource and delegate yourself. Just set the background color of the UIView to your color or image, set the UITableView background color to clear, and then set the UITableView as a subView. For instance in loadView:

// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
[super loadView];
[self.view setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:@"cloud.jpg"]]];
self.tableView = [[[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped] autorelease];
[self.tableView setBackgroundColor:[UIColor clearColor]];
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
[self.view addSubview:self.tableView];
}

With Three20, TTTableViewController isn’t a UITableViewController, so that helps – no major restructuring necessary. Instead all we need to do is override the tableView method on TTTableViewController. Here’s an example of what I did:
///////////////////////////////////////////////////////////////////////////////////////////////////
- (UITableView*)tableView {
if (nil == _tableView) {
_tableView = [[TTTableView alloc] initWithFrame:self.view.bounds style:_tableViewStyle];
_tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth
| UIViewAutoresizingFlexibleHeight;

if (_tableViewStyle == UITableViewStyleGrouped) {
_tableView.backgroundColor = [UIColor clearColor];
[self.view setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:@"cloud.jpg"]]];
}
[self.view addSubview:_tableView];
}
return _tableView;

There might be a slightly better way of doing this with Three20 – in my case, all of my table controllers inherit from my own subclass of TTTableViewController, so it was easy to provide global behavior for my grouped table views.