Bootstrapping Native iPhone Application Development

Below, I summarize essential steps to bootstrap native iPhone application development on a Mac OS X computer. The topics covered are:

The precondition is a phone with sshd installed on it and root access.

The application shows some text and allows the user to drag and flick a circle around the screen. The circle bounces back to the center based on a spring model with damping calculated with a simple Euler integrator. See Glenn Fiedler's articles for a nice and readable introduction to spring physics for games and the Runge Kutta integrator.

The source code illustrates how I worked around a mouse event lag that manifested under rendering load with timers as a rendering trigger. The problem was that timer events would prevent the run loop from processing mouse down events in the queue when new timer events arrived before mouse events could be consumed.

Toolchain Setup

The simplest way to bootstrap iPhone development on a Mac OS X computer is to install the toolchain via MacPorts. Once MacPorts is installed the iPhone toolchain can be pulled simply by entering:

port install arm-apple-darwin-runtime
on the command line. Additionally, a copy of the iPhone's filesystem (dubbed "Heavenly") must be copied to the directory:
/opt/local/arm-apple-darwin/heavenly/
I don't remember whether the MacPorts version of the toolchain sets up the header files required to compile iPhone applications. An alternative source for the header files and a setup script is the iphone-dev toolchain distribution, which can be checked out from Google Code via Subversion. The URL of the Subversion repository is given here.

Before installing the headers contained in the iphone-dev toolchain version, one should verify that the installation script in the include directory copies the headers to the location expected by the port version of the toolchain at:

/opt/local/arm-apple-darwin/include/
Old versions of the includes should be moved aside before running the script.

Essential Makefile

The second line in the Makefile below is essential. Without it, my applications segfaulted on the iPhone right away (Mac OS X Version 10.4.10 on a PowerBook G4). Another important detail of the Makefile is the syslibroot specification and of course the specification of the compiler and linker. Everything else is pretty much boilerplate.


HEAVENLY = /opt/local/arm-apple-darwin/heavenly
export MACOSX_DEPLOYMENT_TARGET = 10.3

APP = MyApp.app
MAIN = MyApp
OBJECTS = \
	MyApp.o \
	MyView.o

CC = arm-apple-darwin-cc
LD = $(CC)

CFLAGS  = -Wall -fsigned-char -ObjC

LDFLAGS = \
	-Wl,-syslibroot,$(HEAVENLY) \
	-lobjc \
	-lc \
	-framework CoreFoundation \
	-framework Foundation \
	-framework UIKit \
	-framework IOKit \
	-framework CoreGraphics \
	-framework CoreSurface \
	-framework GraphicsServices \
	-framework LayerKit

.SUFFIXES: .c .m .h .o
.PHONY: clean package deploy

all: $(MAIN)

$(MAIN): $(OBJECTS)
	$(LD) $(LDFLAGS) -o $@ $^

.m.o:
	$(CC) $(CFLAGS) -c -o $@ $<

.c.o:
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm $(MAIN)
	rm -Rf $(APP)
	find . -name "*.o" -exec rm {} \; -print

$(APP): $(MAIN)
	rm -Rf $(APP)
	mkdir $(APP)
	cp $(MAIN) $(APP)/
	cp PkgInfo $(APP)/
	cp icon.png $(APP)/
	cp Info.plist $(APP)/

package: $(APP)

deploy: package
	scp -r $(APP) root@192.168.22.34:/Applications/
  

Debugging via Syslog

Applications receive certain events only if they are launched through SpringBoard. For instance, if an application is launched from the command line and not from SpringBoard then its

acceleratedInX:(float) Y:(float) Z:(float)
method is not invoked. This means that in some cases, printf style debugging is not available to debug all features of an application. However, the iPhone supports a remote syslog facility via its BSD subsystem, which solves the problem and is also more convenient to use. The setup is explained below.

Configuring Syslog on Mac OS X

The syslog daemon must be stopped and unloaded, program arguments must be added to its launchd configuration file and the daemon must be restarted. Stopping and unloading is done as follows:

$ cd /System/Library/LaunchDaemons
$ sudo launchctl unload -w com.apple.syslogd
Next, -udp_in 1 -bsd_out 1 must be added as program arguments. Each option and value must be enclosed separately in string tags. Then the syslog daemon is loaded and started again as show above, substituting "load" for "unload".

Logging should go to the debug facility, which is configured by adding the following line to the end of the syslogd.conf file in /etc:

user.debug  /var/log/debug.log
The debug log should be created by touching it as root. Debug output from the iPhone can then be viewed conveniently using the Console application.

A word of caution: it appears that syslog traffic is received even if the Mac OS X firewall is on and Internet sharing is off!

Configuring Syslog on the iPhone

Syslog is configured on the iPhone in a similar fashion than on the regular Mac OS X machine. Create a file /etc/syslog.conf and enter the following line in it, with your own machine's IP number substituted in:

user.debug  @192.168.1.10
Stop and unload syslogd on the iPhone as shown above and add a program argument of -bsd_out 1 to its configuration file. The command line argument and the value 1 must be enclosed separately in string tags. Then load the daemon again. In your program, include the syslog header file and add syslog calls to the LOG_DEBUG facility as needed e.g., as shown below:
#include <syslog.h>

syslog(LOG_DEBUG, "My debug message.");

The Example Application

The main application class can be used to set up the GUI. When this is done, the application object should report back to the system that it finished launching. The example application included below additionally intercepts mouse events at the level of the application object and dispatches them to the application's view rather than letting the regular mechanisms dispatch e.g. mouseDown: events to the view.

The reason is that timer events appear to take precedence over mouse events in the regular application runloop. Hence, if a timer is used to trigger the redrawing of a view and the timer fires again before pending mouse events can be consumed then the application may respond sluggishly to mouse (finger) interactions. This is not a problem in this example application but it is a problem in other applications.

My workaround is to run the timer only while the mouse is up, and to consume timer events without rendering if a mouse event is in the event loop. There are certainly better ways to handle this problem and I am sure I'll find them out once Apple releases its SDK ;)

The Application Class

The source code is included below, the header file can be downloaded here.


#import <syslog.h>

#import "MyApp.h"
#import "undocumented-api.h"

#define FRAME_RATE 24.0

int main(int argc, char **argv)
{
  NSAutoreleasePool *pool;
  
  syslog(LOG_DEBUG, "Starting App");
  
  pool = [[NSAutoreleasePool alloc] init];

  UIApplicationMain(argc, argv, [MyApp class]);
  [pool release];
  exit(0);
}


@implementation MyApp

- (void)applicationDidFinishLaunching:(CGEventRef)event
{
  UIWindow *window;
  UIView *content;
  CGRect frame;

  frame  = [UIHardware fullScreenApplicationContentRect];
  window = [[UIWindow alloc] initWithFrame:frame];

  frame   = [window bounds];
  content = [[UIView alloc] initWithFrame:frame];
  myView  = [[MyView alloc] initWithFrame:frame];

  [content addSubview:myView];

  [window setContentView:content];
  [window orderFront:nil];
  [window makeKey:nil];

  [self setSpeed:FRAME_RATE];
  [self reportAppLaunchFinished];
}


- (void)applicationWillTerminate
{}


- (BOOL)handleEvent:(GSEventRef)event
{
  int type;

  type = GSEventGetType(event);

  switch(type)
  {
  case 1:
    [myView mouseDown:event];
    [self stop];
    return NO;
  case 2:
    [myView mouseUp:event];
    [self setSpeed:FRAME_RATE];
    return NO;
  case 6:
    [myView mouseDragged:event];
    return NO;
  }
  return [super handleEvent:event];
}


- (void)setSpeed:(float)fps
{
  if (fps < 1.0 || fps > 50.0)
  {
    return;
  }
  [self stop];

  timer = [
    [NSTimer
      timerWithTimeInterval:((double)1.0 / (double)fps)
      target:self
      selector:@selector(timerFired:)
      userInfo:nil
      repeats:YES
    ] retain
  ];

  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}


- (void)timerFired:(NSTimer *)sender
{
  if (GSEventQueueContainsMouseEvent())
  {
    return;
  }
  [myView timerFired:sender];
}


- (void)stop;
{
  if (timer != nil)
  {
    [timer invalidate];
    [timer release];
                
    timer = nil;
  }
}

@end
  

The View Class

The source code is included below, the header file can be downloaded here.


#import <syslog.h>

#import "MyView.h"

#define FONT_SIZE 48.0
#define DIAMETER 64.0

@implementation MyView

- (id)initWithFrame:(CGRect)theFrame;
{
  if ((self = [super initWithFrame:theFrame]) != nil)
  {
    struct timezone tz;
    
    gettimeofday(&last, &tz);
  }
  return self;
}


- (void)initFont:(CGContextRef)context
{
  CGContextSelectFont(
    context,
    "TimesNewRoman",
    FONT_SIZE,
    kCGEncodingMacRoman
  );
  font = CGFontRetain(CGContextGetFont(context));
}


- (void)drawRect:(CGRect)rect
{
  CGAffineTransform transform;
  CGContextRef context;
  CGPoint center;
  CGRect bounds;
  CGRect box;

  context = UICurrentContext();
  bounds = [self bounds];

  if (font == nil)
  {
    [self initFont:context];
  }
  CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
  CGContextFillRect(context, bounds);

  CGContextSetLineWidth(context, 0.25 * DIAMETER);
  CGContextSetRGBStrokeColor(context, 0.8, 0.2, 0.2, 1.0);

  box = CGRectMake(
    position.x - 0.5 * DIAMETER,
    position.y - 0.5 * DIAMETER,
    DIAMETER,
    DIAMETER
  );

  CGContextAddEllipseInRect(context, box);
  CGContextStrokePath(context);

  CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 0.8);
  CGContextSetFont(context, font);
  CGContextSetFontSize(context, FONT_SIZE);
  CGContextSetTextDrawingMode(context, kCGTextFill);

  transform = CGAffineTransformMake(
    1.0,0.0, 0.0, -1.0, 0.0, 0.0
  );

  CGContextSetTextMatrix(context, transform);

  center.x = bounds.origin.x + 0.5 * bounds.size.width;
  center.y = bounds.origin.y + 0.5 * bounds.size.height;

  CGContextSetTextPosition(
    context, center.x - 0.5 * textWidth, center.y
  );
  CGContextShowText(
    context,
    "Hello",
    5
  );
  /* Cache the text width; there is no good way to figure out the width of
   * some text before rendering it.
   */
  if (textWidth == 0.0)
  {
    CGPoint p;

    p = CGContextGetTextPosition(context);
    textWidth = p.x - center.x;
  }
}


- (void)mouseDown:(GSEventRef)event
{
  CGPoint point;

  point = GSEventGetLocationInWindow(event);
  point = [self convertPoint:point fromView:nil];

  position.x = point.x;
  position.y = point.y;
  velocity.x = 0.0;
  velocity.y = 0.0;

  timestamp = GSEventGetTimestamp(event);
}


- (void)mouseUp:(GSEventRef)event
{
  CGPoint point;
  CGPoint delta;
  double dt;

  point = GSEventGetLocationInWindow(event);
  point = [self convertPoint:point fromView:nil];

  dt = GSEventGetTimestamp(event) - timestamp;

  delta.x = point.x - position.x;
  delta.y = point.y - position.y;

  velocity.x = 0.5 * (velocity.x + delta.x / dt);
  velocity.y = 0.5 * (velocity.y + delta.y / dt);

  [self getTimeDifference];
}


- (void)mouseDragged:(GSEventRef)event
{
  CGPoint point;
  CGPoint delta;
  double dt;

  point = GSEventGetLocationInWindow(event);
  point = [self convertPoint:point fromView:nil];

  dt = GSEventGetTimestamp(event) - timestamp;

  delta.x = point.x - position.x;
  delta.y = point.y - position.y;

  position.x = point.x;
  position.y = point.y;

  velocity.x = 0.5 * (velocity.x + delta.x / dt);
  velocity.y = 0.5 * (velocity.y + delta.y / dt);

  timestamp = GSEventGetTimestamp(event);

  [self setNeedsDisplay];
}


- (void)timerFired:(NSTimer *)timer
{
  CGPoint center;
  CGRect bounds;
  double dt;

  bounds = [self bounds];

  center.x = bounds.origin.x + 0.5 * bounds.size.width;
  center.y = bounds.origin.y + 0.5 * bounds.size.height;

  dt = [self getTimeDifference];

  force.x = SPRING_FORCE * (center.x - position.x);
  force.y = SPRING_FORCE * (center.y - position.y);

  velocity.x = velocity.x * ( 1.0 - SPRING_DAMPING) + force.x;
  velocity.y = velocity.y * ( 1.0 - SPRING_DAMPING) + force.y;

  position.x = position.x + velocity.x * dt;
  position.y = position.y + velocity.y * dt;

  [self setNeedsDisplay];
}


- (double)getTimeDifference
{
  struct timezone tz;
  struct timeval tv;
  struct timeval dt;

  gettimeofday(&tv, &tz);

  dt.tv_sec = tv.tv_sec - last.tv_sec;
  dt.tv_usec = tv.tv_usec - last.tv_usec;

  last.tv_sec = tv.tv_sec;
  last.tv_usec = tv.tv_usec;

  return (double)dt.tv_sec + (double)dt.tv_usec * 0.000001l;
}

@end

  

Other Needed Files

The application also requires a Info.plist file, a PkgInfo file and an icon.png file with a geometry of 59 times 59 pixels. A few header definitions that may not be included in the iPhone toolchain headers is here.