Saturday, November 13, 2010

Creating an iPhone App "From Scratch"

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]; // 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
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> <!-- 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)
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

# 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)
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