Cocoa for Scientists (XXX): Developing for iPhone

Author: Drew McCormack

Around a month ago, I published a book on scientific scripting with Python. Nothing unusual, except the manner in which it was published: the book was only made available via the iTunes App Store.

I wrote a short piece announcing the experiment here on MacResearch, and promised to release the source code for those either wanting to do the same or just learn how you write a simple iPhone app. Today, I am coming good on the promise. This tutorial will be an introduction to developing on the iPhone, using the book reader I developed as an example.

The book reader app.
The book reader app.

The Experiment

Because I already had a completed manuscript, I always considered this exercise an experiment, rather than a genuine attempt to get rich. It cost me relatively little time or expense to publish the book, so there were no great expectations as to how much profit it would generate. A good thing too, because — to be honest — sales haven’t been stellar.

For anyone keeping score, the first month only generated around $500 in profit, and that after being linked to from the high profile Daring Fireball web site. So if you are planning on making a living doing this, a different approach would probably be needed. At least pick a subject matter that forms less of a niche than what I have done, maybe targeting iPhone users more directly.

It should also be noted that many of the big players of tech publishing are starting to get interested in this market. You can now purchase books from O’Reilly in the iPhone application Stanza, and you can download your books from the Pragmatic Programmers in formats supported by iPhone book readers.

The difference between traditional publishers and self publishers lies largely in the price — a book from a publisher is usually $30 or more, where a self publisher can set their price lower (eg my book costs $4.99) — and in the attention to detail. A publisher will usually have several people check a manuscript, including an editor and a technical expert, in addition to the author. A self publisher may not. (A comical example of this is that in my haste to finish, I misspelt the word ‘scripting’ in the title of my book on the app’s startup screen.)

Presentation Engine

Let’s turn now to the app itself, and the choices that I made while developing it. One of the most important issues I faced was the presentation technology that should be used. Book readers like Stanza use standard e-book formats, and layout the text themselves, but these apps have to work with thousands of books. My app only had to work with a single book, so I had complete control over the look.

One option was to use HTML, but HTML does not give you complete control over pagination and the like. I wanted WYSIWYG control, and for that, PDF seemed the best approach. With PDF, you know that what you see in the word processor is what will appear to the reader, and when you are working with source code, that can be a good thing, because you don’t want wrapping or truncation of lines.

There are two options for viewing PDF files on the iPhone. Initially, it seemed that using Core Graphics’ PDF facilities was the best approach, giving the most control. And my first prototype did work very nicely. I developed some cool navigation features where you could skim pages like in iPhoto or iMovie to browse quickly. I was well on the way, but there was a big problem: The PDF functionality in Core Graphics seems to have some serious memory flaws. In short, it accumulates memory, and nothing you do can make it release the memory. After a few minutes browsing around, the app was inevitably killed every time. Even completely releasing the PDF document, and reloading it, did not cure this ailment.

So cool navigation or not, I was forced to look in a different direction: WebKit. WebKit, the engine behind Safari, can display PDF files. The downside is that it doesn’t give you anywhere near as much control as Core Graphics. Even something as fundamental as scrolling the document to a particular page is not self evident. After some effort, I was able to kludge together a solution involving javascript, mixed with a lot of trial and error. That’s the solution I will present below.

Design

In my first article, I gave a brief list of some of the design decisions I took related to how the book should present to the user, and it is worth revisiting those before we get into technicalities. First, I decided that the app should only be viewable in landscape mode. The rationale for this is that I needed as much width as possible to avoid wrapping of source code, and that landscape allowed for a larger font.

A second design choice was that pages should be narrower and longer than standard pages in a physical book. The narrowness — in relation to text size — was obviously to make text readable on the small iPhone screen, but the reason for the page length is less obvious. Because the book has lots of source code examples, I wanted to reduce the number of page breaks that fell within the examples. Longer pages help. Ideally, you would just have one continuous page, but having pagination aids in navigation, so it still serves a purpose.

Finally, navigation is much more important in a technical book than, say, a novel. Many people use technical books for reference purposes, rather than reading cover-to-cover. In short, I figured an easily accessible and detailed table of contents was a must.

The Table of Contents screen.
The Table of Contents screen.

Becoming an iPhone Developer

Now that we have covered the basic specs of the book reader, we can start to look at how you go about developing one for the iPhone. However, before you can do that, there are a few preliminaries we need to cover so that you can follow along with the tutorial.

First, you have to sign up as a Registered iPhone Developer. This is free, and gives you access to the tools you need to develop for the iPhone. Once you have done that, download and install the latest iPhone SDK (2.2 at time of writing). This installs the Xcode tools, and various iPhone specific frameworks and documentation. It also installs the iPhone simulator, which is how you test your application most of the time.

If you want to get serious about developing for the iPhone, and actually want to start distributing your apps through the iTunes App Store, you will need to enroll in the iPhone Developer Program. This costs $99 a year, and you may need to wait several weeks before you are admitted. If you wish to install your apps on an iPhone, rather than just in the simulator, you need to join this program.

Source Code

With the iPhone SDK installed, you can download the source code for the book reader app. Unzip the archive, and double click the Xcode project to get started.

The Interface

The ‘Books’ app is just about the simplest iPhone app you could imagine — flashlight apps aside — so it is a good place to start learning about iPhone development. There are only a handful of classes: the obligatory application delegate, and two so-called view controller classes. All of these classes get instantiated via the MainWindow.xib Interface Builder file, which is loaded on launch.

The Main Window Interface Builder file.
The MainWindow.xib file is used to instantiate all controller objects.

If you open the MainWindow.xib file, and double click any of the view controller objects, you will see that the views they control are actually in other Interface Builder files. For example, if you double click on ‘Books View Controller’, you will see a window that indicates that the view is loaded from the file ‘BooksViewController.nib’. (In actual fact, it is loaded from the ‘BooksViewController.xib’, a small bug in Interface Builder.) If you open the BooksViewController.xib file from the Resources group in Xcode, you can see the main view of the application.

The main view.
The main view of the app.

The BooksViewController is the file owner of this XIB file, and the top level view object is automatically assumed to be the controllers view object. You can very easily create a view—controller IB file like this by choosing File > New File… in Xcode: locate the iPhone OS section in the new file pane, and choose User Interfaces. Lastly, select the View XIB icon, and finish up by naming the new file.

The view in the BooksViewController.xib file contains a UIWebView, and a single button, which brings up the table of contents. The target of the button is the BooksViewController object, and the action is showTableOfContents:.

View Controllers

Even from this brief look through the Interface Builder files, it is clear that the iPhone is quite a different beast to the Mac. The mechanics of building an app are similar, but the UI classes are completely different. Indeed, an iPhone interface is built on a framework called UIKit, rather than the AppKit that is used to develop Mac apps. Developing with UIKit is no more difficult than developing with AppKit — in fact, it is somewhat simpler because Apple have been able to fix design decisions that are set in stone in AppKit — but even the most experienced Mac developer will need a little time to get used to the different approach.

One of the most fundamental differences between developing on the Mac and developing on the iPhone is that the iPhone makes extensive use of view controllers. Any top level view that is displayed on the screen will tend to be associated with a particular instance of UIViewController, or a subclass thereof (eg UITableViewController). These view controllers are responsible for loading the view, responding to interaction from the user, and unloading the view when it is no longer needed.

The iPhone is a very modal device. In contrast to the Mac, an iPhone app tends to be made up of a hierarchy of modal screens. Think about an app like Mail: you drill down through layers of hierarchy — account, mailbox, message — each layer associated with a different view. And each of these views has its own view controller. In fact, its the view controllers that get connected together to form the hierarchy; each view gets displayed onscreen as its corresponding view controller is moved to the top of the view controller stack.

Table Views Everywhere

Another big departure for the iPhone from the Mac is its use of Table Views. Whenever you see a list on the screen, you are unquestionably looking at a UITableView object. Table Views are not only used for simple lists, but can be used to present graphical content or settings. For example, the beautiful book shelf in the Classics app is a table view, and the whole Settings app is no more than pages and pages of UITableView instances.

Settings in a table.
Settings are usually presented in a UITableView

In our book reader, a table view is used for the Table of Contents. The TableOfContentsViewController class is a subclass of UITableView.

#import <UIKit/UIKit.h>

@class BooksViewController;

@interface TableOfContentsViewController : UITableViewController {
    NSDictionary *tableOfContentsDictionary;
    IBOutlet BooksViewController *booksController;
}

@property (assign) IBOutlet BooksViewController *booksController;

@end

This class loads a property list in the method viewDidLoad, which is then used to populate the table.

-(void)viewDidLoad {
    [super viewDidLoad];

    // Load Table of Contents
    NSString *tocPath = [[NSBundle mainBundle] pathForResource:@"TableOfContents" ofType:@"plist"];
    tableOfContentsDictionary = [[NSDictionary dictionaryWithContentsOfFile:tocPath] retain];
}

The viewDidLoad method, along with methods like viewWillAppear: and viewDidAppear:, are very common in UIViewController subclasses. They offer a good place to setup or load elements of the UI just before or after it comes on screen.

It may come as a surprise to some that view classes on the iPhone do not support bindings. As you become more experienced in iPhone development, the reasons for this become more evident, but initially it seems an odd choice. Instead, you use traditional Cocoa patterns like delegates and data sources to populate views. The UITableView class has many of these methods, most of which are optional. Here are a few of the data source methods from TableOfContentsViewController (for the full list, see the source code).

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[tableOfContentsDictionary objectForKey:@"chapters"] count];
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSDictionary *chapterDict = [[tableOfContentsDictionary objectForKey:@"chapters"] objectAtIndex:section];
    return [[chapterDict valueForKey:@"sections"] count] + 1; // Add one for the chapter header
}

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    NSDictionary *chapterDict = [[tableOfContentsDictionary objectForKey:@"chapters"] objectAtIndex:section];
    return [chapterDict objectForKey:@"name"];
}

-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [tableOfContentsDictionary valueForKeyPath:@"chapters.label"];
}

-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
    return index;
}

Most of these methods just access the data in the property list, and return that to the table view. They are fairly self explanatory, though there are aspects that differ from the NSTableView objects you find on the Mac. For example, table views on the iPhone can have sections. I have used a section for each chapter in the book reader. Another example of sections is the Contacts app list, which has one section per letter of the alphabet.

Cells Are Not What You Think

One of the most important table view data source methods is tableView:cellForRowAtIndexPath:. This method actually has to return the view that is used to display a given row.

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"TableOfContentsCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
    }

    NSDictionary *chapterDict = [[tableOfContentsDictionary objectForKey:@"chapters"] objectAtIndex:indexPath.section];
    if ( indexPath.row == 0 ) {
        NSString *s = [NSString stringWithFormat:@"%@. %@", 
            [chapterDict objectForKey:@"number"], 
            [chapterDict objectForKey:@"name"]];
        cell.text = s;
    }
    else {
        NSDictionary *sectionDict = [[chapterDict objectForKey:@"sections"] objectAtIndex:indexPath.row-1];
        NSString *s = [NSString stringWithFormat:@"%@.%@ %@", 
                       [chapterDict objectForKey:@"number"], 
                       [sectionDict objectForKey:@"number"],
                       [sectionDict objectForKey:@"name"]];
        cell.text = s;
    }

    return cell;
}

This code can be difficult to comprehend the first time you see it if you have any experience developing on the Mac. The first thing to remember is that the cells referred to here have basically no relation to the cells you use for drawing with NSTableView. Cells on the iPhone are true views — UIView subclasses — and remain onscreen as long as needed. On the Mac, cells are just a lightweight drawing class, and do not remain on screen or respond to events.

To prevent too many cells being created, and causing memory problems, each table view maintains a queue of reusable cells: When a cell goes offscreen, it is kept alive in the queue, and reused when a new cell is needed. The following code checks if there is a cell available to reuse, and if not, creates a new one.

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero 
                   reuseIdentifier:CellIdentifier] autorelease];
    }

The rest of the tableView:cellForRowAtIndexPath: method populates the cell with data. In this example, only the cell’s text property is set, but in other cases, you might set the image property, or even create a custom cell with many text and image elements.

Working with UIWebView

The BooksViewController takes care of loading the PDF document into the UIWebView, and navigating it. The viewDidLoad method locates the PDF, creates an NSURLRequest, and uses the loadRequest: method to load it into the view.

    // Load PDF
    NSString *path = [[NSBundle mainBundle] pathForResource:BKPDFFileName ofType:@"pdf"];
    NSURL *url = [NSURL fileURLWithPath:path];
    NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
    [webView loadRequest:urlRequest];

I couldn’t find any simple way to scroll to a particular page. (If anyone knows one, please post a solution in the comments.) The rather ugly solution I found was simply to use trial and error to determine the length of a page, and use javascript to scroll to a particular coordinate.

-(void)updateView {
    [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"window.scrollTo(0, %d);", self.scrollPosition]];
}

This code runs a one line Javascript script that requests the window to scroll to a particular coordinate. The value stored in the scrollPosition property is determined using this formula

-(NSInteger)offsetForPage:(NSInteger)pageIndex {
    return (pageIndex - 1) * BKPixelsPerPage + 10;
}

with the BKPixelsPerPage constant determined by trial and error.

Do It Yourself

That covers the core functionality of the app. If you want to know more detail, I suggest just getting your hands dirty in the source code.

I’m releasing this source under the BSD license, which means you can pretty much do what you like with it, as long you attribute the code to me. (See the license file in the Xcode project.) If you decide to publish your own book using the source code, here is what you will need to do:

  1. Create a document like the one I made in Pages, with the same or similar page layout and text formatting.
  2. Generate a PDF from the document, and include it in the Xcode project.
  3. Change the value of the BKPDFFileName variable in BooksViewController.m to point to your PDF file.
  4. Change the data in the TableOfContents.plist file so the table of contents is correct.
  5. Double click the ‘Python’ target, and change the name to something suitable for your book.
  6. In the Build settings of the target, change the ‘Product Name’ setting.

That should cover most of it. If you do actually publish something on the App Store, let us know so we can see if it is appropriate for listing on MacResearch.