Programming

Software | Secret Software | Writing

How I got into iPhone development

Hello world. With the launch of iPhone 2.0, this little article suddenly got popular - and a bit kind of irrelevant, unfortunately. What I write about here is programming a jailbroken iThing using a Javascript interpreter, whereas everyone these days wants to do things with the official SDK and write apps that run on non-jailbroken iThings. Jiggy is still a nice easy way to write your own apps in a homebrew hacker way, but it won't get you something you can sell on iTunes and Make Money Fast. Sorry.

A while back I picked up an iPod Touch. I wanted a wireless PDA, and the iPod seemed like a good one, with the music player being a nice bonus feature. So of course the first thing I did was take it home and jailbreak it and install a bunch of handy stuff on it - a Japanese-English dictionary, the Bible, Wikipedia, VNC, Mobile RSS, you know the kind of thing.

And now I had a PDA, I could start organising my insanely-large todo list, which is normally kept in a file on my computer. But if I have a file on my computer and a file on my PDA, they're guaranteed to get out of sync. A friend of mine had recently introduced me to Hiveminder (http://www.hiveminder.com/) - well, he wrote it - and so I stuck all my tasks in there. It has a nice feature whereby I can download a text file with my tasks in it, fiddle with it, upload it again and Hiveminder will take care of syncing everything. I can just dump the file into my iPod Notes application, and off I go.

Or at least, I could, if only there were a nice way to get data in and out of the Notes application.

But hey, it's a computer, and computers are for programming. So that's what I did. I wrote myself a little iPhone application to reach into Notes.app, pull out the Hiveminder todo list, and then sync it with the web service.

Getting settled in

Note that when I began this enterprise I knew nothing about iPod/iPhone programming. (I'm going to refer to the platform as "the iPhone" from now on, even though I've only got an iPod, because the programming side of things is the same.) I knew there was an unofficial SDK at http://code.google.com/p/iphone-dev/, and so I downloaded it and built myself a toolchain. Then I learnt Objective C, set up the clever XCode template, compiled up the Hello World application, and started pawing my way through some example code and reading up on the APIs. It was a long day.

And a wasted day, because while I was doing some Google searches about how to do stuff, I came across Jiggy (http://www.jiggyapp.com/). Jiggy is a Javascript interpreter and framework for iPhone apps. It means you can write your application in Javascript using Jiggy's libraries to interface with the iPhone's system.

You can install Jiggy and the runtime libraries from Installer.app. The Jiggy application itself gives you a Web-based IDE for creating iPhone apps; connect to a HTTP server on your iPhone from your desktop and get working away.

I didn't use that. I used it briefly to create the application skeleton, and did the rest by SSH'ing into the device and using vi on the main.js file it had created. There are three main reasons for this. One, I'm old-school. Two, it gives me a faster develop-test-debug cycle. Three, by running jiggy from the command line in SSH, I get to see all the debugging output.

Next, I got an idea of what I wanted the interface to look like. I needed to ask the user's username and password, and I needed a big friendly button saying "Sync". I also opted for a Hiveminder logo, and a progress bar to give some user feedback about the sync.

Here's the UI, taken from the finished application:

Next I had to build it.

UI in the iPod

There's no GUI interface builder for iPod applications; you code all the UI by hand. The UIKit is based on a view-and-subview model. Your application's main window forms a view, and widgets are added on top of this view.

The code to create the main window is pretty much the same for every application unless you're doing very clever stuff, and it looks like this:

    Plugins.load( "UIKit" );
    var window = new UIWindow( UIHardware.fullScreenApplicationContentRect );
    window.setHidden( false );
    window.orderFront();
    window.makeKey();
    window.backgroundColor = [ 1 , 1 , 1 , 1 ];
    var mainView = new UIView();
    window.setContentView( mainView );

We've created a new window, brought it to the front, and painted it white. Then we create a view to put our widgets in. You can have multiple views and swap them in and out, but we don't need that. We just need one. And it needs some stuff in it. First thing is that navigation bar at the top of the window:

    var nav = new UINavigationBar( [ 0 , 0 , 400 , 48 ] );
    nav.showButtonsWithLeftTitle("Sync", "Sync", false);
    mainView.addSubview( nav );

Jiggy's API documentation tells you what widgets are available for you to play with, but it often doesn't tell you what they are, so you have to experiment. I knew from the other API digging I had done that UINavigationBar was that nice blue bar at the top of the application window with the buttons on it. We create a new one and tell it where it's going to live; a widget's frame is made up of X co-ordinate, Y co-ordinate, width and height.

I also knew from the API documentation that showButtonsWithLeftTitle lets you define the labels for the buttons on the left and right hand side of the navigation bar, and that if you passed an empty string into one of those parameters, the button would not be displayed. Unfortunately Jiggy doesn't support this bit, and if you pass an empty string in, you get a blank button. Not attractive. We work around this by making the navigation bar wider then the iPhone's screen size. This is bad and wrong and a hack. But it works for now, and by the time iPhones have 640x480 resolution I'm hoping Jiggy will be fixed.

Anyway, this is the first thing we put in the main view.

Next we define a progress bar and our Hiveminder logo image:

    var progress = new UIProgressBar ([ 10, 360, 300, 38 ]);
    progress.setStyle(1);
    progress.setProgress(0);

    var image = Images.imageAtPath("/Applications/Hiveminder.app/splash.png");
    var imageView = new UIImageView( image );
    imageView.frame = [ 10, 400, 300, 55 ];

Notice that you have to hard-code your frame co-ordinates. You can get an array of the display boundaries with UIHardware.mainScreenSize, find the image height with image.size, and so on, but you still have to sit down either with trial and error or a pencil and paper to work out what should go where.

The preferences are the hard bit, but thankfully, Jiggy documentation has a very detailed example of how to create a preferences table. (http://jiggyapp.com/download/examples/ezpreferencestable.js) It even tells you to split off half of that example into a separate file and include it, which is exactly what I did.

    include("ezprefs.js");

Then I defined my preferences table, basically copying again from the example:

    var ezptabledef = [{
        title: "User details",
        titleHeight: 40,
        cells: [
            { id: "username", title: "Email address" },
            { id: "password", title: "Password" },
        ]
    }];

    var ptable = new EZPreferencesTable( [ 0, 48, 320, 410 ] , ezptabledef );
    ptable.reloadData();

Finally, I added all these wonderful new widgets to my application's view:

    mainView.addSubview( ptable );
    mainView.addSubview( progress );
    mainView.addSubview( imageView );

Running this application gives us the main Hiveminder application view. Doesn't it look pretty? Of course, it doesn't do anything yet. That comes next.

Preferences and defaults

First, though, let's fix up that preferences dialog a bit. We can already start adding our username and password to it, even though it doesn't do anything. However, we will notice two things straight away: first, we have to put our username and password in every time, and that gets boring fast, and second, the password is displayed in the clear. That's not good. Let's fix these.

First, the password thing. In the ezprefs class, we can Do Things to a preferences cell by adding a customize attribute to that cell's description. So, we turn:

    { id: "password", title: "Password" }

into

    { id: "password", title: "Password", 
        customize: function (cell) { cell.textField.secure = 1 }
    }

customize provides a function which is called after the preference cell is set up. We use that function to grab the text field part of the cell, and set its secure attribute to true. Now when we enter stuff in the password field, it gets displayed as stars instead of plaintext. That's half the job done.

Now to remember the username and password for later invocations. Mac OS X has a "defaults" system, where applications can store preferences in a structured database that only the most unfair would call a "registry". No. It's a "defaults" system.

And the iPhone, since it runs OS X, also has a defaults system. Jiggy provides a nice interface to the defaults system. To get a value from the defaults system, you say Defaults.get(key). And to store a value, you say Defaults.set(key, value). Let's assume that our username and password is already stored in the defaults system. First we'll again alter the preferences table to prime its data from default, and then second we'll make our assumption a reality.

ezprefs allows us to set the text value of our preferences cells with another attribute, so we'll set this to the values we got out of Defaults. This remains replacing what we have at the moment:

    { id: "username", title: "Email address" },
    { id: "password", title: "Password", 
        customize: function (cell) { cell.textField.secure = 1 }
    }

With this:

    { id: "username", title: "Email address", 
        text: { value: Defaults.get("username") } },
    { id: "password", title: "Password", 
        text: { value: Defaults.get("password") },
        customize: function (cell) { cell.textField.secure = 1 } 
    }

Now we need to make sure that these defaults get saved. The best time to save them is when the "Sync" button is pressed, so let's see how we respond to button events.

Our navigation bar was in a variable called nav. To assign an action to it, we create the following function:

    nav.onButtonClicked = function(bar, button) {
    }

bar gives us the navigation bar object. We don't need that. And button gives us the index of the button that was pressed. But we hid the right hand button off the screen, so only the left-hand button could be pressed. And even if you could see the right-hand one, that would do the same anyway. So we ignore that one too.

Once the button has been pressed, we can store the username and password into defaults:

    nav.onButtonClicked = function(bar, button) {
        Defaults.set("username", ptable.getPreferenceValue("username"));
        Defaults.set("password", ptable.getPreferenceValue("password"));
    }

Notice again that we're using methods from the ezprefs object. It's a handy thing.

Now we have an application that stores our Hiveminder username and password. But it still doesn't do anything with them.

Interfacing with Notes

The aim of the game is to have a note in the Notes application which stores all of our Hiveminder tasks. With that in mind, we'd better find out how Notes stores its notes.

ssh'ing into my iPod - sorry, let me just say that again, because it still fills me with awe - ssh'ing into my iPod, I find a file in /var/media/Library/Notes/ called notes.db. This is a SQLite database; SQLite is a very nifty SQL database engine which happens to be the basis for part of Apple's "Core Data" framework. Copying it home and using the sqlite3 command line utility to examine it:

    % sqlite3 notes.db
    SQLite version 3.4.0
    Enter ".help" for instructions
    sqlite> .schema
    CREATE TABLE Note (creation_date INTEGER, title TEXT, summary TEXT);
    CREATE TABLE _SqliteDatabaseProperties (key TEXT, value TEXT, UNIQUE(key));
    CREATE TABLE note_bodies (note_id INTEGER, data, UNIQUE(note_id));
    CREATE TRIGGER delete_note_bodies AFTER DELETE ON Note
    BEGIN
    DELETE FROM note_bodies WHERE note_id = OLD.ROWID;
    END;

We see two main tables; Note and note_bodies. I think we can make a fairly well-educated guess that "Note" holds the information about notes in the database, and note_bodies fills out each note with the textual content of the note in question.

Our application will somehow need to connect to this SQLite database, potentially create a new note with the Hiveminder todo list, or if it already exists, send it to the Hiveminder server, sync it to reflect any changes since the iPhone last connected, and receive a new sync'ed version.

Thankfully, Jiggy has a SQLite library plugin for this very purpose. We can connect to an SQLite database, we can check for the existence of a given record, we can create a record if necessary, and then we can retrieve and/or update a given record using Javascript methods, thanks to the SQLite plugin.

First things first, let's use the SQLite library:

    Plugins.load("SQLite");

And then connect to our database, and see if we have a Hiveminder entry already:

    db = new SQLite( "/var/mobile/Library/Notes/notes.db" );
    note = db.select( "select ROWID , title from note where title like '%Hiveminder Todo%'" );

SQLite.select returns an array of arrays. If we already have a todo list, then this will be a one element array containing a two element array. If not, it'll be empty.

Interfacing with Hiveminder

Let's now briefly think about how to sync our data with Hiveminder. Syncing two distinct data sources is difficult, but thankfully we can leave the heavy lifting to HM; as far as we're concerned, we need to think about three distinct steps:

  1. First, if we already have a to-do list, send it to Hiveminder. HM will determine whether our list has been changed, and if so, work out how to synchronise the two sources. We check that went OK.
  2. If we don't already have an entry in the database, we need to make an empty one at this stage.
  3. We get the synchronised list from HM, and place it in the database.

We turn that lot into code and put it into our onButtonClicked function, which now looks like this:

    nav.onButtonClicked = function(bar, button) {
        Defaults.set("username", ptable.getPreferenceValue("username"));
        Defaults.set("password", ptable.getPreferenceValue("password"));
        if (!logIn())  { return }

        db = new SQLite( "/var/mobile/Library/Notes/notes.db" );
        note = db.select( "select ROWID , title from note where title like '%Hiveminder Todo%'" );

        var mess;
        if (note && note.length) mess = sendOurs();
        if (mess) {
            if (!note || !note.length) makeOne();
            if (getTheirs()) alert(mess, "Sync complete");
        }
    };

We've assumed the existence of four functions here: logIn, sendOurs, makeOne and getTheirs. All of these require talking to the Hiveminder server, which uses a rather peculiar API. But it's a web service API, and wouldn't you know it, Jiggy comes with an implementation of XMLHttpRequest, which is fast becoming the standard way for Javascript to talk to Web servers, and Hiveminder returns us data in JSON format, which can be evaluated into Javascript. So let's set up a function to communicate with Hiveminder:

    function doHMAction (url, p, name) {
        xhr.open("POST", "http://hiveminder.com/"+url, false);
        xhr.send(p);
        log("POSTING "+url+" : "+p);
        if (xhr.failed) { alert(name+" failed - " + xhr.statusText); return; }
        var resp;
        log(xhr.responseText);
        eval("resp = ["+xhr.responseText+"]");
        if (resp[0]) { resp = resp[0] }
        if (resp["fnord"]) { resp = resp["fnord"] };
        log(resp);
        if (!resp.success) { alert(name+" failed - " + resp.error); return; }
        return resp;
    }

We begin by posting some data to a Hiveminder URL. We expect the reponse text to be JSON, so we evaluate it into a variable called resp. Sometimes Hiveminder wraps its replies in a hash key called "fnord", so we unpack these in this case. Now talking to the web site is fairly easy if we know how the REST API works:

    function logIn() {
        var credentials = "J:A:F-address-fnord="+Defaults.get("username");
        credentials += "&J:A:F-password-fnord="+Defaults.get("password");
        credentials += "&J:A-fnord=Login";
        if (doHMAction("__jifty/webservices/json", credentials, "Login")) {
            progress.setProgress(0.25);
            progress.updateIfNecessary();
            return true;
        }
        return false;
    }

If we get a successful response out of the login call, we're a quarter of the way to having our data synchronised - and so we update the progress bar accordingly. If not, we're going to return false, which is going to cause our calling function to bug out with an error message.

There and back again

Next, if we've got data to send, we send it:

    function sendOurs() {
        var body = db.select( "select data from note_bodies where note_id = "+note[1][0]);
        var data = body[1][0];
        if (!data) { return myAlert("Couldn't get the todo list data from Notes");}

Even though we're selecting one row and one column, the data comes back from the db.select call as an array of arrays, and the first row is a set of column headers. We ignore those and get at the todo list data. Then we need to mangle the data a bit - Notes stores things in basic HTML, and we want to be sending plain text. Once we tidy that up a little, we can send the task data to Hiveminder's API endpoint:

        data = data.replace(/<br>/g, "\n");
        var resp = doHMAction("=/action/UploadTasks.json", "format=sync&content="+escape(data), "Sending tasks");
        if (resp) { return resp["message"]; }
    }

Of course this is what we need to do if we already have a task list in Notes. But if this is the first time we're using the application, we won't have anything there at all. So we need to make a note, which means putting some rows in the notes database:

    function makeOne () {
        log("Making a HM note entry");
        db.execute( "INSERT INTO note values (1, 'Hiveminder Todo list', '')");
        note = db.select( "select ROWID , title from note where title like '%Hiveminder Todo%'" );
        if (!db.execute( "INSERT INTO note_bodies values ("+note[1][0]+", 'xx')" ))
            alert("Couldn't create body for HM note")
    }

First we write to the log - the handy log function will write text out on standard error if we call jiggy from the command line of the iPhone. Then we create our note, find it again and then stuff some dummy text in it. We're going to fill it with the real task list in a moment.

    function getTheirs() {
        var resp = doHMAction("=/action/DownloadTasks.json", "format=sync", "Getting tasks");
        if (!resp) { return false }
        var todo = resp.content["result"];
        todo = todo.replace(/\n/g, "<br>");
        todo = todo.replace(/"/g, "\"\"");

We get the content and change it back into pseudo-HTML. Also since we're about to put it into an SQL statement, we need to escape the double quotes. There are other things we should escape as well, but unfortunately Jiggy's SQLite implementation doesn't allow us to use data placeholders. Until it does, we have to do the escaping ourselves.

        var rowId = note[1][0];
        var sql = 'UPDATE note_bodies SET data = "'+todo+'" WHERE note_id = '+rowId
        if (!db.execute(sql)) {
            alert("Couldn't update the database");
            return false;
        }
        progress.setProgress(1);
        progress.updateIfNecessary();
        return true;
    }

Anyway, we place the task list we got from Hiveminder into the database, and then we're done.

That's all

With all that in place, we've just created a native iPhone application, and a useful one at that - it will allow me to take my to-do list onto the road, work with it offline, and then synchronise it back to Hiveminder when I'm on the network again. And it wasn't too hard, either! Big thanks to Jiggy for providing a very easy way to build applications for the iPhone, and to Jesse and Hiveminder for keeping me organised...

Latest articles

Development activity

This page was last checked for correctness on 2008-07-15. Contact Simon.