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:
- Setting up the toolchain
- Configuring syslog on the iPhone and a Mac
- Makefile magic
- Application with timers, custom animation and mouse input
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
/opt/local/arm-apple-darwin/heavenly/
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/
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)
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
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
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
#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.