Current as of: ~Jan. 2011 (iOS 4.2 and XCode 3.2.5).
Download source code.
This tutorial explains how to compile, build, and deploy to the Simulator an iPhone application without using Xcode or Interface Builder. I don't claim that this is the best way to go about developing without Xcode/IB, but, it works well for me.
I will attempt to explain things as I go along, however, you'd best be advised to read through the code and, in particular, read the comments in the code.
Pros (subjective!):
- Better grasp of the basics and what's "under the hood".
- More control over the build process and the user interface.
- Possibly better integration with standard Unix development tools.
- Less reliance on the mouse. Useful as I sometimes develop on a Netbook where the UI/mouse is a real pain. Allows for more efficient development with, say, just a fullscreen emacs and fullscreen terminal, which also fits my development tools on other platforms.
- Lack of dependence on Xcode toolsuite.
- Probably others.
Cons (subjective!):
- No Xcode benefits such as "intellisense" (though some editors support this).
- Syntax highlighting and formatting dependent on your editor (though almost all editors support this).
- Debugging probably both more complicated and more labor-intensive.
- Probably others.
TO DO:
- Figure out how to script the simulator to start an app automatically on launch.
- Improve the monitoring of the NSLog log file (probably with a tail
script).- Add some scripts and pre-processor macros for GDB integration.- Figure out how to use a Prefix file for common includes- Determine minimum entries required in Info.plist.- Determine the absolute minimum compilation and linking flags and variables.- Put together a Makefile and script for deployment to an iPhone.- Change $DIR
var in Makefile to not use pwd
.- Determine why "User" and "OS" deployment locations differ.
CHANGES:
8.9.2010 - N/A. Initial release.1.29.2011 - Updated for iOS 4.2. (minor bug fix in Makefile)
Contents
1. The App Proper
Files: main.m BasicApp.m BasicApp.h
Though technically it is not necessary to seperate these files, it is rather wise to do so as a foundation for a larger application where such partitioning is necessary. These files constitute what is just shy of the absolute minimum for an iPhone app using the standard Apple framework.
These files are pretty straight-forward. First, we create a basic entry point (main.m
), then tell it to go grab our application delegate, a basic class that inherits a protocol from UIApplicationDelegate (BasicApp.m/.h
). Our class, in turn, programatically creates a UIWindow with a basic UIView attached, sets a background color for the view, and displays the window. Done.
main.m
#import <UIKit/UIKit.h>
int main (int argc, char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int r = UIApplicationMain(argc, argv, @"UIApplication", @"BasicAppDelegate");
[pool release];
return r;
}
BasicApp.h
#import <UIKit/UIKit.h>
@interface BasicAppDelegate: NSObject {
UIWindow *mainWindow;
UIView *mainView;
}
- (UIWindow *) getMainWindow;
@end
BasicApp.m
#import "BasicApp.h"
@implementation BasicAppDelegate
- (void) applicationDidFinishLaunching: (UIApplication *) application {
mainWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
mainView = [[UIView alloc] initWithFrame: [[UIScreen mainScreen] applicationFrame]];
[mainWindow addSubview: mainView];
[mainView setBackgroundColor: [UIColor lightGrayColor]];
[mainWindow makeKeyAndVisible];
}
- (void) applicationWillTerminate {
[mainWindow release];
}
- (UIWindow *) getMainWindow {
return mainWindow;
}
@end
2. The Info.plist
Files:
Info.plist
An application under iOS/OSX usually consists of more than the application binary. In reality a ".app" is a directory with a specific structure. For example, when we finish, our application bundle will look like this:
.app is a directory
~/BasicApp.app > ls -l
total 20
-rwx------+ 1 user domain group 15384 2010-08-08 18:51 BasicApp
-rwx------+ 1 user domain group 806 2010-08-08 18:51 Info.plist
~/BasicApp.app >
If you examine other applications, you will notice plenty of other items, such as sub-directories for cache, images, data, screenshots, etc., however, the minimum requirements for an iPhone app are a) the application binary and b) the Info.plist.
When you copy an app onto the Simulator, the Simulator interrogates the Info.plist to determine things such as what the application name is, what the binary is to launch, what kind of region the app was designed for, any special styles you want to apply, the version number, etc. There are many options for the Info.plist, however, the important ones are CFBundleExecutable
and CFBundleIdentifier
.
The Info.plist is just XML, however, one thing to keep in mind is that the iPhone can accept either a binary "compiled" Info.plist or just a plain text plist. Xcode by default uses a compiled plist while we will use a regular text one (mostly because I can't really be bothered to figure out the compilation step ;)).
Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC
"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>BasicApp</string>
<key>CFBundleIdentifier</key>
<string>com.whatever.SecondTest</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleBlackTranslucent</string>
<key>UIViewEdgeAntialiasing</key>
<string>YES</string>
</dict>
</plist>
3. CompilingXcode doesn't do anything very mysterious here. After all, this is just Unix, and it is using mostly standard Unix applications. However, rather than delve too deep into the specifics of gcc
and build up the compilation step by hand, we will take the easy way out: cheat!
It turns out that all Xcode is really doing is using the xcodebuild
application, which is sort of Apple's replacement for make
. Not only that, but xcodebuild
can actually be quite verbose and tell you exactly how it is going about it's business (you can see the same info in an Xcode window but I find the command line to be clearer).
Try this: go create a new default app in Xcode, save it, build it, and run it. Pretty straight-forward, right? Now, quit Xcode, navigate to the directory of the new app, and run xcodebuild
to see what is going on (you may have to run xcodebuild clean
first):
Part of the compilation output from xcodebuild
CompileC build/Untitled.build/Release-iphonesimulator/Untitled.build/Objects-normal/i386/UntitledAppDelegate.o Classes/UntitledAppDelegate.m normal i386 objective-c com.apple.compilers.gcc.4_2
cd /Users/x/Documents/Untitled
setenv LANG en_US.US-ASCII
setenv PATH "/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/git/bin:/usr/X11/bin"
/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.2 -x objective-c -arch i386 -fmessage-length=0 -pipe -std=c99 -Wno-trigraphs -fpascal-strings -fasm-blocks -Os -mdynamic-no-pic -Wreturn-type -Wunused-variable -D__IPHONE_OS_VERSION_MIN_REQUIRED=30200 -isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator3.2.sdk -fvisibility=hidden -mmacosx-version-min=10.5 -gdwarf-2 -fobjc-abi-version=2 -fobjc-legacy-dispatch -iquote /Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Untitled-generated-files.hmap -I/Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Untitled-own-target-headers.hmap -I/Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Untitled-all-target-headers.hmap -iquote /Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Untitled-project-headers.hmap -F/Users/x/Documents/Untitled/build/Release-iphonesimulator -I/Users/x/Documents/Untitled/build/Release-iphonesimulator/include -I/Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/DerivedSources/i386 -I/Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/DerivedSources -DNS_BLOCK_ASSERTIONS=1 -include /var/folders/Yv/YvDzynCUEIaYVYJCCeIbHU+++TI/-Caches-/com.apple.Xcode.501/SharedPrecompiledHeaders/Untitled_Prefix-bzdssktcospdfwenxzjipogpifzt/Untitled_Prefix.pch -c /Users/x/Documents/Untitled/Classes/UntitledAppDelegate.m -o /Users/x/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Objects-normal/i386/UntitledAppDelegate.o
So, the idea goes, if we examine this output, we can figure out what all the required compilation steps and flags are and assemble them into some start of the venerable Makefile
.
Beginnings of a Makefile
PATH=/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Developer/usr/bin: \
/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/git/bin:/usr/X11/bin: \
/Developer/usr/bin:/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin
MACOSX_DEPLOYMENT_TARGET=10.6
CC=/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.2
CFLAGS=-x objective-c -arch i386 -fmessage-length=0 -pipe -std=c99 -Wno-trigraphs \
-fpascal-strings -fasm-blocks -Os -mdynamic-no-pic -Wreturn-type -Wunused-variable \
-D__IPHONE_OS_VERSION_MIN_REQUIRED=30200 \
-DNS_BLOCK_ASSERTIONS=1 \
-isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.0.sdk \
-fvisibility=hidden -mmacosx-version-min=10.6 -gdwarf-2 \
-fobjc-abi-version=2 -fobjc-legacy-dispatch
SRCS = \
main.m \
BasicApp.m
Now, there are some other things that xcodebuild
is doing here on the compilation step, but they are not required, so we will skip them for now.
4. Linking
Similar to the compiling step above, we will examine the output of xcodebuild
to put together the linking portion of the Makefile
. The linking step will assemble the individually compiled source files from above and combine them into an actual application binary.
Example linking output from xcodebuild
Ld build/Release-iphonesimulator/Untitled.app/Untitled normal i386
cd /Users/p/Documents/Untitled
setenv MACOSX_DEPLOYMENT_TARGET 10.5
setenv PATH "/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/git/bin:/usr/X11/bin"
/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.2 -arch i386 -isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator3.2.sdk -L/Users/p/Documents/Untitled/build/Release-iphonesimulator -F/Users/p/Documents/Untitled/build/Release-iphonesimulator -filelist /Users/p/Documents/Untitled/build/Untitled.build/Release-iphonesimulator/Untitled.build/Objects-normal/i386/Untitled.LinkFileList -mmacosx-version-min=10.5 -Xlinker -objc_abi_version -Xlinker 2 -framework Foundation -framework UIKit -framework CoreGraphics -o /Users/p/Documents/Untitled/build/Release-iphonesimulator/Untitled.app/Untitled
If we example this output we can determine what sort of flags, commands, and variables are necessary to properly link an iPhone app together.
Additions to our Makefile
DIR:=$(shell pwd)
LDFLAGS=-arch i386 -mmacosx-version-min=10.6 -Xlinker -objc_abi_version -Xlinker 2 \
-isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.0.sdk \
-L$(DIR) \
-F$(DIR) \
-framework Foundation \
-framework UIKit \
-framework CoreGraphics
OBJS := $(SRCS:.m=.o)
5. DeployingThe iPhone Simulator has a local file system where it stores, among other things, the applications. Any valid application copied to this location will show up on the Simulator home screen when you launch the Simulator. There are two locations for applications, one at the user level and one at the "OS" level:
- ~/Library/Application\ Support/iPhone\ Simulator/4.0/Applications/
- ~/Library/Application\ Support/iPhone\ Simulator/User/Applications/
I have come across problems before, for reasons yet unknown, using the "User" directory so I use the "OS" directory. It doesn't really matter so go with whatever works.
Some Makefile additions
SIMULATOR=/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\ Simulator.app
APPDIR=~/Library/Application\ Support/iPhone\ Simulator/4.0/Applications/
There is really only one last required step to deploy to the Simulator: setting up the file structure of the app.
Above we examined the file structure of a standard ".app". Our file structure for deployment will be the same with one addition: a parent directory who's name is a UUID. This unique identifier allows one to deploy multiple versions of the same app on a phone while still being able to keep them distinct. There don't seem to be any real rules regarding the generation of this UUID (the formula can accept a value as the seed/hash but it is not necessary). It can remain constant or it can change on every compilation, whichever you prefer (Xcode changes it on every fresh build/deploy).
Fortunately there is a standard function on OSX to generate a UUID:
~/BasicApp > /usr/bin/uuidgen
4663FCE9-0675-432B-8390-1E17D122859C
~/BasicApp >
Since this will be the parent directory, our final file structure for deployment will look like:
(dir) 4663FCE9-0675-432B-8390-1E17D122859C
(dir) ... BasicApp.app
... BasicApp
... Info.plist
Copy this to one of the Simulator deployment locations listed above and start the simulator!
6. Putting It All Together (Draft 1)
File: Makefile
Below is the first draft of our Makefile
. Essentially it includes all of the steps listed above plus a few more to actually make it do something. As long as you have main.m, BasicApp.m, BasicApp.h, and Info.plist all in the same directory as this Makefile
, you should be good to go. Just run either make
(to build and deploy), make clean
(clean out local build files and the app files on the Simulator), or make sim
(start the simulator).
Makefile, Draft 1
APPNAME=BasicApp
UUID=4663FCE9-0675-432B-8390-1E17D122859C
SRCS = \
main.m \
BasicApp.m
PATH=/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin: \
/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/git/bin: \
/usr/X11/bin:/Developer/usr/bin:/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin
MACOSX_DEPLOYMENT_TARGET=10.6
SIMULATOR=/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\ Simulator.app
APP_LOC= ~/Library/Application\ Support/iPhone\ Simulator/4.0/Applications/$(UUID)
DIR:=$(shell pwd)
CC=/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.2
CFLAGS=-x objective-c -arch i386 -fmessage-length=0 -pipe -std=c99 -Wno-trigraphs \
-fpascal-strings -fasm-blocks -Os -mdynamic-no-pic -Wreturn-type -Wunused-variable \
-D__IPHONE_OS_VERSION_MIN_REQUIRED=30200 -DNS_BLOCK_ASSERTIONS=1 \
-isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.0.sdk \
-fvisibility=hidden -mmacosx-version-min=10.6 -gdwarf-2 -fobjc-abi-version=2 \
-fobjc-legacy-dispatch
LDFLAGS=-arch i386 -mmacosx-version-min=10.6 -Xlinker -objc_abi_version -Xlinker 2 \
-isysroot /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator4.0.sdk \
-L$(DIR) \
-F$(DIR) \
-framework Foundation \
-framework UIKit \
-framework CoreGraphics
OBJS:=$(SRCS:.m=.o)
all: prep Application
prep: ;
@cd $(DIR); \
mkdir -p $(UUID); \
mkdir -p $(UUID)/$(APPNAME).app
Application: $(OBJS)
$(CC) $(LDFLAGS) -o $(UUID)/$(APPNAME).app/$(APPNAME) $^
sed 's/BasicApp/$(APPNAME)/g' Info.plist > $(UUID)/$(APPNAME).app/Info.plist
cp -R $(UUID) ~/Library/Application\ Support/iPhone\ Simulator/4.0/Applications/
%.o: %.m
$(CC) $(CFLAGS) -I. -c $< -o $@
sim: ;
open $(SIMULATOR)
clean:
rm -rf $(UUID)
rm -rf *.o
rm -rf $(APP_LOC)
7. Improving NSLog
TBD. Note: Check the source code for the beginnings. It's mostly complete (Debug.m/.h)
8. Improving The Makefile
TBD. Note: Check the source code for the extra portions of the Makefile. Explanation to follow.
9. Putting It All Together (Draft 2)
TBD