iTunes App driven by WordPress plugins

Last year, Top Quark was commissioned by Adam Oliver Brown of the University of Ottawa to create a glossary plugin for his WordPress site.  He wanted it for his french language biology course at the university.  This year, he wanted to get the glossary into the App Store.  We were both very excited when we got word from Apple the other day about the app being live and ready for purchase.

So, si tu parles français and want to study up on your biological terms, Adam’s team has built a very impressive glossaire of about 1300 termes.  Splurge the $1.99 and download it from the app store.

It was fun to build an app store app that can talk to a plugin running a WordPress app.  Fun!  And difficult enough to demand serious, fist-pounding, hair-pulling, shoulda-gone-to-bed-hours-ago WTF coding.

There are several technologies at play.  At the core of Top Quark’s contributions to the process is a Top Quark Architecture plugin that provides an API for the other plugins to hook into.  topCore (aka topArchitecture), can run stand-alone and has the potential to port to Drupal or Joomla, but for now it is best run in WordPress.  On top of that architecture is built the topGlossary plugin, which runs a multi-lingual capable glossary, aforely mentioned re: Adam Oliver Brown.  The services that topCore provides to topGlossary include: import/export using CSV (unit tested to 10,000 records); DB encapsulation with a handy whereClause class; user-based permissions as to which users/subscribers have access to make changes; and enough hooks and filters to programmatically add fields, alter displays and make other customizations to topGlossary without hacking into source code.  The final piece on the WordPress side is a third topPlugin which provides services specific to answering cross-domain JSONP calls.  If all goes well, that will be merged into topCore.

On the App side stands a pair of great open source projects. The first is the bridging code PhoneGap, which allows a web app (i.e. IE)(sic) to run natively on all of the mobile phone platforms (iPhone, BlackBerry, Android, etc).  The second is Sencha Touch, the javascript framework for building mobile web applications. Both presented their challenges, but were overall very satisfying tools.

Working with Phonegap

There were three areas that took problem solving. I worked off of Phonegap 0.9.4.

1. Default.png flashing strangely when the app loads

This seems to be a known bug and I’m sure the Phonegap developers will be fixing it shortly, but in the meantime, when loading the app in the iPad simulator, the iPhone Default.png file flashed, sometimes (depending on startup orientation) rotating the screen wildly as it did.  There are lots of fixes out there that seemed to work for the developers who posted them, but didn’t necessarily work for anyone else.  I thought the least I could do would be to post mine.  I made two changes to PhonegapDelegate.m (which on my computer is sitting in /User/{me}/Documents/PhoneGapLib/Classes):

To stop the strange rotation of the screen if starting on an iPad in different orientations, within didFinishLaunchingWithOptions, I commented out the following line:

[supportedOrientations objectAtIndex:0] intValue]]; // Trevor – commenting this out resolved the flash of rotated screen .
For the flashing of Default.png, I tried for hours (despite internet posts and comments that told me what I was trying to do was impossible, but then sometimes it just goes that way), to detect the iPad orientation (
[[UIDevice currentDevice] orientation]
or
UIInterfaceOrientation

). It seems the interweb offered me the answer, and I slapped it in the face, only to be defeated on the field of honour.

So, what I ended up with was a hodge podge of various Google found code snippets and my own virginly tinkering with Objective C.  Also within didFinishLaunchingWithOptions (in PhoneGapDelegate.m), I ended up hacking it so that the secondary splash image (the various Default*.png files) simply doesn’t display if starting on an iPad.  Instead, a black screen appears and then fades to the web app when the latter is ready.

/*
* imageView – is the Default loading screen, it stay up until the app and UIWebView (WebKit) has completly loaded.
* You can change this image by swapping out the Default.png file within the resource folder.
*/

// Top Quark – added this in to make sure the iPad splash image loads properly (and doesn’t flash Default.png)
// http://www.topquarkproductions.ca/2011/03/my-first-iphone-app/

CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ];

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// For iPad users, I will not display a secondary splash image because I can’t figure out how to
// display the correct one.  Instead, I’ll just display a black screen
imageView = [[UIView alloc] initWithFrame:screenBounds];
imageView.backgroundColor = [UIColor blackColor];
}
else{
UIImage* image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@“Default” ofType:@“png”]];
imageView = [[UIImageView alloc] initWithImage:image];
[image release];
}
imageView.tag = 1;
[window addSubview:imageView];
[imageView release];

The fade to the web app was achieved by editing {MyApp}AppDelagate.m.  For that, I adjusted the webViewDidStartLoad and webViewDidFinishLoad methods to:

/**
Called when the webview finishes loading.  This stops the activity view and closes the imageview
*/

(void)webViewDidFinishLoad:(UIWebView *)theWebView
{
[UIImageView beginAnimations:nil context:NULL];
[UIImageView setAnimationDuration:1.0f];

theWebView.alpha = 1.0f;

[UIView commitAnimations];
return [ super webViewDidFinishLoad:theWebView ];
}

(void)webViewDidStartLoad:(UIWebView *)theWebView
{
theWebView.alpha = 0.0f;
return [ super webViewDidStartLoad:theWebView ];
}

2. Shipping a SQLite database file with the app

It would have been possible to ship the app without any data and have it self-populate via calls to the server. But, of course, that would be a horrible user experience. So, we had to somehow ship the glossary data with the app. This turned out to be wonderfully easy thanks to a running start from a post on Google Groups.

Sencha Touch, for the time being anyway, does not have a SQLite Proxy. It has all sorts of other proxies, for dealing with data within the constructs of the framework, but no SQLite. Despite that, when I began looking into a javascript framework for the app, Sencha Touch seemed to generate a more native feel than jQTouch (which was the other option). However, being unable to use a SQLite database, I had to use LocalStorage. It worked fine.

Here’s all you have to do:

  1. Somehow get your database loaded into LocalStorage. For the glossary app, the same code that handles getting updates from the server can also be used to query the entire glossary. So, the first time the Phonegap app runs in the simulator, it builds the LocalStorage database as it will be shipped.
  2. Find the LocalStorage file on your computer. I first got the Phonegap app running within the iPhone simulator (which generated the LocalStorage), then found the file at /Users/{me}/Library/Application Support/iPhone Simulator/{my_simulator_iOS_Version>/Applications/{my_app_id}/Library/Webkit/LocalStorage/file__0.localstorage
  3. Add the LocalStorage file to your XCode Project

Now, when you compile and run your PhoneGap app, you have access to that file as a resource. Within the init method of my GlossaryAppDelegate.m file (yours would be named for your app), I added the following:

//Copy over the database if it doesn’t exist
databaseName = @“file__0.localstorage”;

// Get the path to the Library directory and append the databaseName
NSArray*libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString*libraryDir = [libraryPaths objectAtIndex:0];

// the directory path for the file__0.localstorage file
databasePath = [libraryDir stringByAppendingPathComponent:@“WebKit/LocalStorage/”];

// the full path for the file__0.localstorage file
databaseFile = [databasePath stringByAppendingPathComponent:databaseName];

// Execute the “checkAndCreateDatabase” function
// Create a FileManager object, we will use this to check the status
// of the database and to copy it over if required
NSFileManager*fileManager = [NSFileManager defaultManager];

// Check if the database has already been created in the users filesystem
NSString*databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:databaseName];
if([fileManager fileExistsAtPath:databasePathFromApp] && ![fileManager fileExistsAtPath:databaseFile]){
// Create the database folder structure
[fileManager createDirectoryAtPath:databasePath withIntermediateDirectories:YES attributes:nil error:NULL];

// Copy the database from the package to the users filesystem
[fileManager copyItemAtPath:databasePathFromApp toPath:databaseFile error:nil];
}
[fileManager release];

…immediately before the line:

return [super init];

Et voilá, the database file is now packaged with the app and can be used within the web app via regular javascript LocalStorage calls.

3. Links within the app opening within the app

This was a little annoying. Web links within the app opened within the app, but provided no navigation tools (back button, address bar, y’know, the basics). In order to get them to open in the phone’s browser, within the GlossaryAppDelegate.m file, I updated the following function in the following way:

/**
* Start Loading Request
* This is where most of the magic happens… We take the request(s) and process the response.
* From here we can re direct links and other protocalls to different internal methods.
*/

(BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//theWebView.alpha = 0.0f;
NSURL *url = [request URL];
if( [[url scheme] isEqualToString:@“http”] || [[url scheme] isEqualToString:@“https”])
{
[[UIApplication sharedApplication] openURL:url];

return NO;
}
else{
return [ super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType ];
}
}

Working with Sencha Touch

1. Communicating with the WordPress server

A major requirement of the app store app was to be able to retrieve updates from the server running on www.adamoliverbrown.com, such that Adam and his team could get updates and then push those updates to the glossary app on the iPhone.  I didn’t end up pushing as much as allowing the app to pull the updates from the server.  Alas, the app does will not have home screen badge privileges.

In this project I finally understood something about cross-domain javascript calls (JSONP as opposed to JSON).  An app running on an iPhone is running in a different domain than Adam’s WordPress site.  Thus, your standard AJAX calls (be they GET or be they POST) don’t work because of browser security issues.  The way apps get around that is that they dynamically generate JavaScript snippets that get loaded into the DOM via SCRIPT tags.  That means that said snippets have to execute meaningful JavaScript as opposed to merely passing back a JSON object.

There are two pieces needed to make such a thing work.  The first is the JavaScript code within the document that calls back to the server.  The second is the PHP code on the server that generates the dynamic JavaScript code.

Here’s a chunk of the code I used to load the terms originally from the server (as opposed to

/***********************
* load_terms(offset,limit,callback)
* – offset – a specific point in the index (start at 0)
* – limit – number to fetch per call ( so as not to overwhelm the server)
* – callback – a function to call whenever the JSONP result has loaded
***********************/

var termsJSONPSent
var termsJSONPReset;
function load_terms(offset,limit,callback){
if (termsJSONPSent == true){
// already running  Only do one at a time.
if (offset == 0){
termsJSONPReset = true;
}
return;
}
termsJSONPSent = true;
Ext.util.JSONP.request({
url: myLiveServerURL, // resides on the WordPress Server
callbackKey: ‘callback2′, // note: this gets referenced in the PHP script
params: {
action: ‘something_kinda_secret’,
what  :‘terms’,
offset : offset,
limit : limit
},
callback: function(result) { // this function gets defined in global namescape
termsJSONPSent = false;
if (termsJSONPReset){  // If things got reset earlier, just start again
termsJSONPReset = false;
load_terms(0,limit,callback);
return;
}
if (typeof callback == ‘function’) callback(result);
if (result.finished){
finalize_loading();
}
else{
for(var i = 0; i < result.length; i++){
localStorage.setItem(‘t’+result[i].id,JSON.stringify(result[i]));
}
if (result.length < limit){
finalize_loading();
}
else{// check for more
load_terms(offset+result.length,limit,callback);
}
}
}
});
}

This is a recursive function to fetch the list of glossary terms from the WordPress server in batches of `limit` (I used 100 as my limit). The function accepts a third argument `callback` which allows for a way to do stuff with the results.

The WordPress server then generates a piece of JavaScript code like this:

if ($_SERVER[‘SCRIPT_FILENAME’] == __FILE__ and $_GET[‘action’] == ‘something_kinda_secret’){
// If this is being accessed directly, then it’s a cross-domain ajax call.
header(“Content-type: text/javascript”);
ob_start();
GlossaryApp_Ajax($exit = false);
$data = ob_get_clean();
echo $_GET[‘callback’].“(“.$data.“);”;
exit();
}

// The following exist so the app can run as a native web app in the WordPress site as well.
add_action(‘wp_ajax_glossary_ajax’,‘GlossaryApp_Ajax’);
add_action(‘wp_ajax_nopriv_glossary_ajax’,‘GlossaryApp_Ajax’);
function GlossaryApp_Ajax($exit = true){
global $GlossaryPackage;
$GlossaryTermContainer = new GlossaryTermContainer();
$TagContainer = new TagContainer();

$DefaultLanguage = current($GlossaryPackage->languages);
switch ($_REQUEST[‘what’]){
case ‘terms’:
$wc = new whereClause();
$wc->addCondition($GlossaryTermContainer->getColumnName(‘GlossaryTermLanguage’).‘ = ?’,$DefaultLanguage);
$limit = isset($_REQUEST[‘limit’]) ? $_REQUEST[‘limit’] : 1; // limit to 1 term
$offset = isset($_REQUEST[‘offset’]) ? $_REQUEST[‘offset’] : rand(0,$GlossaryTermContainer->countAllObjects($wc) 1); // get a random one

$Terms = $GlossaryTermContainer->getLimitedGlossaryTerms($wc,$limit,$offset);
$return = array();
if (count($Terms)){
foreach ($Terms as $Term){
$return[] = $Term->getParameters(); // returns an array of info about the term, with an `id` param
}
}
else{
$return[‘finished’] = true;
}
break;
}
echo json_encode($return);
if ($exit) exit();
}
?>

When that gets loaded into the DOM via a <script> tag, it runs the callback we declared in the JSONP call above and passes it a JSON encoded array of terms, where each term is an associative array with an `id` param. The callback, as it’s written above, will place each term in the returned array into LocalStorage, stored at the index ‘t’+term.id. The term is stored as a stringified (aka serialized()ed) associative array.


What was that Mr. Schrödinger?

So this classically under construction site has been live and stagnant already for a longer half-life than I care to admit. Let’s get it observed so it can collapse into what it’s going to be.

Right now it must be considered all possible websites at once. It is, at the same time, part repository for Top Quark WordPress Plugins, part support forum and part marketing tool for a little mobile app start-up called Top Quark Productions.  Here will be a destination to find the following WordPress plugins:

  • A suite of WordPress plugins, ideal for festivals and conferences, to turn a multi-day, multi-venue, multi-talented itinerary into a Phonegap-integrated, customizable, app-store ready app
  • An extension to Formidable Pro a popular forms plugin, adding a new “Table” field type
  • A fork of an old email newsletter friend, poMMo, turned into a powerful and extensible contact management system
  • A Top Quark Architecture plugin (free – used by the app and poMMo plugins), along with an invitation for open source co-development

If you’re interested in keeping in touch with us about Top Quark Plugins, please leave a comment, follow @topquarky on Twitter or send an email to info@topquarkproductions.ca.

Status of the Conference App

The conference app has been successfully deployed as a web app.  The Phonegap integration is not done yet, but is high on the roadmap.  There are real-world examples of the web app available online.  They work on iPhone, Android and Blackberry Touch phones (older Blackberry’s can access a non-javascript version).  To see it in action, on the desktop or mobile, visit:

  • A proof-of-concept done in an afternoon while waiting to play a song with Digging Rootsat the National Aboriginal Acheivement Awards in Edmonton (March, 2011)
  • A production version covering the Folk Alliance International in Memphis (February, 2011)
  • A bilingual version covering the Canadian Arts Presenting Association conference in Ottawa (November, 2010).  The bilingual aspect of this version was programmed as an extension to the core plugins
  • Where it came together for the first time, for the Ontario Council of Folk Festivalsconference in Ottawa (October, 2010)
  • A richer example of what the core plugins can do on the desktop for the Ashkenaz Festival in Toronto (September 2010)
  • Several Top Quark plugins, including the poMMo & Formidable extensions, are in production for the Mariposa Folk Festival (July, 2011)