Original URL: http://www.theregister.co.uk/2007/11/14/iphone_app_explained/

How I wrote an iPhone application

An introduction to the world of mobile Web 2.0

By Bill Ray

Posted in Mobile, 14th November 2007 10:56 GMT

If an El Reg hack can write an iPhone application then anyone can, so we thought we'd have a go and pass on our experience in the hope that others can build something more useful.

In proper Web 2.0 fashion we decided to create an application which would achieve nothing and have no commercial potential. Therefore, our application would load The Register website and parse its contents to remove all references to the ubiquitous iPhone, replacing the term with a string of the user's choice.

First thing was to download the iPhone documentation from the Apple Developer Connection, which required us to sign up with a valid email address and left us with a small pile of pdf documents to read.

Unfortunately, these documents proved almost entirely useless as they talked at length about the style and usability of iPhone applications, but said very little about how to write them. The JavaScript guides were a bit more useful, but not a lot, and we got more mileage out of our rather elderly "JavaScript Definitive Guide" (circa 1997) and various JavaScript websites.

We also needed to download the latest version of Safari - you're supposed to be able to develop using Safari then deploy straight onto an iPhone, though life is not really that simple, and that does need XP or Vista (or a nice Mac, but our budget wouldn't stretch).

Assuming one isn't going down the native-applications-for-unlocked-iPhones-only route, then iPhone applications are written in JavaScript - an annoying scripting language with aspirations of objecthood and no significant relationship to Java. JavaScripts can't live alone - they are embedded in web pages - but as we wanted our app to load other web pages we made that page a frameset (which can contain other pages).

All our development was done using PFE - we eschewed more complex environments for what should be a bit of quick text editing.

Initially, our frameset only contained a single frame, to load the El Reg pages into, but we had to add a second frame when it became clear that the iPhone's "back" button wasn't going to operate as we would have liked. But even with the second frame for the menu bar our HTML page managed to keep it simple:

<HTML>
  <HEAD>
    <TITLE>Test using frames</TITLE>
    //JavaScript goes here
  </HEAD>
  <FRAMESET ROWS="*,32" FRAMEBORDER="no" onLoad="startUp();">
    <FRAME SRC="blank1.html" NAME="left">
    <FRAME SRC="menubar.html" name="menu">
  </FRAMESET>
</HTML>

Then it was just a matter of inserting our script into the header with a function called "startUp" to be run when the page had loaded, the script is listed in its entirety at the end of this article. blank1.html is just a holding text, while menubar.html contains our additional back button and an option to change the replacement text.

Before our first function we created a few variables for later use:

var myRequest = new XMLHttpRequest();
var changeTo = "";
var home = 'http://www.theregister.co.uk';

JavaScript isn't strongly typed, but it should be clear that the latter two are strings and the first is an object-a-like thing we'll discuss later.

The StartUp function just checks to see if the change-text is set, and if not it asks the user what it should be set to. The functions for creating, changing and deleting cookies were lifted verbatim.

function startUp() {
  changeTo = readCookie("newName")
  if (changeTo == null) {
    changeTo = window.prompt("So what would better suit the iPhone?");
    createCookie("newName", changeTo, 1);
  }
  loadRegister(home);
}

The "loadRegister" function is run when the user clicks on a link (thanks to some later code) and checks to see if the user wants to leave The Register. If so, the page they want is loaded and the app ceases to run. Otherwise, the desired page is loaded using an XMLHttpRequest object-a-like.

function loadRegister(targetURL) {
  var targetDomain = targetURL.substring(targetURL.indexOf(".", 8)+1, targetURL.indexOf("/", 8));
  if (home.indexOf(targetDomain) == -1) {
    alert("Moving Off Site: " + targetDomain);
    parent.parent.location=targetURL;
  }
  myRequest.open("GET", targetURL);
  myRequest.onload = targetLoaded;
  myRequest.send();
}

Now we're in less-well-trodden territory, as XMLHttpRequest isn't as consistently supported as most of JavaScript, so documentation is sparse and contradictory in places. The basic function of an XMLHttpRequest is to load web pages, but it's worth noting that it can only load pages which come from the same server as the script itself - in this case "www.theregister.co.uk". Otherwise it just sits around doing nothing and reporting no error unless you're using desktop Safari with the JavaScript console turned on, an essential step which makes bug-chasing possible, if still horribly difficult. The line...

myRequest.onload = targetLoaded

...asks that once the desired page has finished loading the function targetLoaded should be run, this extracts the text and changes every instance of "iPhone" in the document (where preceded by, or followed with a space). It then opens, writes to, and closes the main frame to display the page before launching a truly horrible hack.

function targetLoaded() {
  var loadedSite = myRequest.responseText;
  loadedSite = loadedSite.replace(/iPhone /g, changeTo + " ");
  loadedSite = loadedSite.replace(/ iPhone/g, " " + changeTo);

  var loadedDocument = parent.frames[0].document;
  loadedDocument.open();
  loadedDocument.write(loadedSite);
  loadedDocument.close();

  setTimeout('pageLoaded()', 10000);
}

What we would like to do is run through each of the links on the page adding an "onclick" command to intercept mouse clicks. The problem is that we can't do that until the page has loaded, and the JavaScript event system isn't up to telling us when that's happened (as the browser considers the frame to have already loaded once and it's damned if it's going to trigger another "onload" event).

On desktop Safari we can use a hack with window.find to see if the end of our text has loaded, checking every few seconds to see if the words "Copyright 2007" are present, but that doesn't work on the iPhone for some reason. So our only alternative was to wait for ten seconds and then assume the text has loaded into the frame. This generally seems to work, but you wouldn't want to rely on it for anything mission-critical.

Once we think the whole document has loaded we can loop though and append our instruction to each link:

function pageLoaded() {
  for (i=0; i < parent.frames[0].document.links.length; i++) {
    parent.frames[0].document.links[i].onclick = linkClicked;
  }
}

When we were testing, the El Reg home page had 241 links on it, so it might be possible to count them (in order to avoid the wait-ten-seconds hack) but only if the number of links on the page could be guaranteed not to change.

This just left us with an old frames problem: the back button won't work, as it insists on considering the master frame to be the thing you want to back away from. We mucked about for a bit trying to get pages to load in different windows, which again works fine on the desktop but is verboten on the iPhone version of Safari, where only a user clicking can open a new window.

To solve the back-button problem we were forced to create our own back button, which does work, and was supposed to sit in a different frame at the bottom of the screen. But the iPhone Safari considers the whole frameset to be a single document, so when the user scrolls they scroll around the whole document rather than one frame: putting our toolbar at the bottom of the document rather than the bottom of the screen.

But we do have a working application which will also function on the Nokia implementation of Safari, but looks very odd on the desktop version.

Mainly we discovered that JavaScript is really irritating to program in, and that the idea of write-once-run-anywhere remains as true as it ever was. We also got annoyed at the security restrictions and the lack of comprehensive documentation, though at least the latter problem should fade with time.

We also decided that we're not going to develop anything else for the iPhone until there's a proper development kit, allowing the use of a proper programming language, and some decent documentation too. ®

<script language="JavaScript">
//<!-- JavaScript here
  
  //This one thinks it's an object
  var myRequest = new XMLHttpRequest();
  
  //This is the text we're going to change the word "iPhone" to
  var changeTo = "";
  
  //This is our home page, and the site that leaving will unload the app
  var home = 'http://www.theregister.co.uk';
  
  function startUp() {
    changeTo = readCookie("newName")
    if (changeTo == null) {
      changeTo = window.prompt("So what would better suit the iPhone?");
      createCookie("newName", changeTo, 1);
    }
    loadRegister(home);
  }
  
  function loadRegister(targetURL) {
  
    //targetDomain is set to a string containing the site (but not directories or file) that the user clicked on
    var targetDomain = targetURL.substring(targetURL.indexOf(".", 8)+1, targetURL.indexOf("/", 8));

    //We compare that to our home page
    if (home.indexOf(targetDomain) == -1) {
      alert("Moving Off Site: " + targetDomain);
      //This line unloads this application, as the targetURL replaces this document
      parent.parent.location=targetURL;
    }

    //Then we load the page
    myRequest.open("GET", targetURL);
    myRequest.onload = targetLoaded;
    myRequest.send();
  }
  
  function targetLoaded() {
    var loadedSite = myRequest.responseText;
    
    loadedSite = loadedSite.replace(/iPhone /g, changeTo + " ");
    loadedSite = loadedSite.replace(/ iPhone/g, " " + changeTo);

    var counter;

    var loadedDocument = parent.frames[0].document;
    
    loadedDocument.open();
    loadedDocument.write(loadedSite);
    loadedDocument.close();
    //This is our horrible bodge which waits 10 seconds for the page to load
    setTimeout('pageLoaded()', 10000);
  }
  
  function pageLoaded() {
    //This loops through every link on the page (241 on the El Reg home page when we were testing this) and adds an "onclick" even listener
    for (i=0; i < parent.frames[0].document.links.length; i++) {
      parent.frames[0].document.links[i].onclick = linkClicked;
    }
  }
  
  function linkClicked() {
    loadRegister(this.href);
    //We return "false" so the browser dosen't attempt to load the link clicked on.
    return false;
  }
  
  function returnHome() {
    loadRegister(home);
  }
  
  function changeName() {
    eraseCookie("newName");
    changeTo = window.prompt("So what would better suit the iPhone?");
    createCookie("newName", changeTo, 1);
  }
  
  function createCookie(name,value,days) {
        if (days) {
                var date = new Date();
                date.setTime(date.getTime()+(days*24*60*60*1000));
                var expires = "; expires="+date.toGMTString();
        }
        else var expires = "";
        document.cookie = name+"="+value+expires+"; path=/";
  }
  function readCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for(var i=0;i < ca.length;i++) {
                var c = ca[i];
                while (c.charAt(0)==' ') c = c.substring(1,c.length);
                if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
        }
        return null;
  }
  function eraseCookie(name) {
        createCookie(name,"",-1);
  }
//-->
</script>