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:
- Download JSON nodes from Drupal and store them in MongoDB
- 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.