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.
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.
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];
// Start up the application and point it's delegate to our BasicAppDelegate class
// (BasicApp.m/.h
). UIApplicationMain(int argc, char *argv[],
// NSString *principalClassName, NSString *delegateClassName)
int r = UIApplicationMain(argc, argv, @"UIApplication", @"BasicAppDelegate");
[pool release];
return r;
}
BasicApp.h
#import <UIKit/UIKit.h>
@interface BasicAppDelegate: NSObject {
// These are the class variables we will use for our UIWindow and UIView objects.
// We could have just as easily declared these in BasicApp.m
but decided not to.
// Your call.
UIWindow *mainWindow;
UIView *mainView;
}
// The UIApplicationDelegate protocol requires you implement this function.
// It will return our main UIWindow so the application (UIApplicationMain from main.m
)
// knows where to start.
- (UIWindow *) getMainWindow;
@end
BasicApp.m
#import "BasicApp.h"
@implementation BasicAppDelegate
- (void) applicationDidFinishLaunching: (UIApplication *) application {
// Note that mainScreen
is a class method that returns the device's screen
// and is provided by the platform by default.
// Create the window and set it's size to the maximum.
mainWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
// Determine the max size allowed for the view. This is based off of the application
// size. This should be something like the max size minus the status bar up top.
mainView = [[UIView alloc] initWithFrame: [[UIScreen mainScreen] applicationFrame]];
// Add the view to our window and set the color of the view
// (the color change is really only necessary to make it more obvious
// that we've succeeded).
[mainWindow addSubview: mainView];
[mainView setBackgroundColor: [UIColor lightGrayColor]];
// Display the window and make it the "key" window. The "key" window is the one
// that will receive user input.
[mainWindow makeKeyAndVisible];
}
- (void) applicationWillTerminate {
[mainWindow release];
}
- (UIWindow *) getMainWindow {
return mainWindow;
}
@end
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>
<!-- CFBundleExecutable needs to be the name of the actual application binary,
which, in this example, is BasicApp. -->
<key>CFBundleExecutable</key>
<string>BasicApp</string>
<!-- CFBundleIdentifier needs to be a UNIQUE name for your app. This is
not the same thing as the display name. -->
<key>CFBundleIdentifier</key>
<string>com.whatever.SecondTest</string>
<!-- This stuff is rather meaningless. Not sure yet if required. -->
<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>
<!-- These are optional settings that define look-and-feel.
Things like this can usually be set in the application code, however, those
statements will not be executed until the application is run. These settings
take effect before your application is ever displayed. -->
<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
# First-off, it looks like we need to set the PATH, so we'll just copy that over from
# xcodebuild
. Note that it is including the standard Unix bin directories
# as well as the iPhone Platform directories.
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
# Second, we need to set the platform (not sure if this is required).
MACOSX_DEPLOYMENT_TARGET=10.6
# Third, by examining the beginning of the xcodebuild
output, it looks like the compiler
# that's being used is gcc 4.2. We will use absolute pathing just to be sure. Note this is a
# different location than some previous Xcode versions.
CC=/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/gcc-4.2
# Now we need to dig into the gcc-4.2
flags. It doesn't really matter what these flags stand
# for, provided we make sure there's nothing glaringly wrong, since we are just copying
# xcodebuild
to be safe. Note, however, that we are compiling for the Simulator
# (i.e., local computer, i.e., 386) and that we have to point to the Simulator
# SDK. If you are going to compile for deployment these flags WILL change.
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
# Of course, we also need to define which source files are going to be included in our binary!
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
# First, we'll just define a convenience variable to hold our directory. This probably
# should be changed from pwd
in case the Makefile
is
# executed outside of the directory.
DIR:=$(shell pwd)
# We are not going to call the linker directly. Instead we will ask gcc to invoke the linker.
# This is why some of the flags here look similar to the compilation step. The most importing
# thing here, other than calling the linker, is to set isysroot
to the Simulator SDK and to
# make sure that we are including all the necessary frameworks. If you build off of this app
# you will likely need to add additional frameworks here as you add additional features.
#
# NOTE: these flags (in particular, the framework flags) require the PATH set as per above!
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
# This is a standard make
substitution command using the SRCS
variable that was defined
# above. In other words, our compiled objects are the same as the source files, but with
# .o
extension instead of .m
.
OBJS := $(SRCS:.m=.o)
- ~/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
# The location of the Simulator. This will be different if you are using a
# previous SDK version.
SIMULATOR=/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone\ Simulator.app
# Location where to deploy the application to for the Simulator.
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
# Edit this config info below.
APPNAME=BasicApp
# use /usr/bin/uuidgen to generate a unique UUID for this app
UUID=4663FCE9-0675-432B-8390-1E17D122859C
SRCS = \
main.m \
BasicApp.m
# you shouldn't need to change anything below this line
# (unless you need to add frameworks to the linker)
##########################################################
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)
##########################################################
# Before we build the application, run some prep commands.
all: prep Application
# We need to make sure that we create the sub directories to hold our to-be-deployed app.
prep: ;
@cd $(DIR); \
mkdir -p $(UUID); \ # Create UUID dir and ".app" sub-dir
mkdir -p $(UUID)/$(APPNAME).app
# Link our application, generate the Info.plist file, and copy the dir to the Simulator.
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/
# Compile our source files.
%.o: %.m
$(CC) $(CFLAGS) -I. -c $< -o $@
# Launch the simulator.
sim: ;
open $(SIMULATOR)
# Remove all object files, remove the application directory, and remove the app
# from the Simulator.
clean:
rm -rf $(UUID)
rm -rf *.o
rm -rf $(APP_LOC)
plutil can be used to convert a plist between XML and binary formats
ReplyDeleteThis is great stuff, thanks for putting it together.
ReplyDeleteYou might also want to checkout xcrun, it's a CLI program by Apple (like xcodebuild) that helps you "run or locate development tools."
It could eliminate the need for hardcoded SDK and simulator paths.
Is there a way to cross-compile this BasicApp under X86-Linux?
ReplyDeleteIs this how-to also valid for iPad apps?
ReplyDeleteawesome find resourceful stuff you have shared
ReplyDeletethanks!!!
.....
Hi, I'm also doing the same thing so I wanted your advices about my work...
ReplyDeletehttp://code.google.com/p/aie-wow/downloads/list