After a conversation with some co-workers, I discussed how it would be technically possible to write an iPhone app using only a main.m file—no separate class files. This post is the result of that. It’s definitely doable, but not something I’d ever recommend for shipping code.
The code is explained below, but you can get the full source in my public GitHub repository.
Getting Started
By default, an iPhone application template gives you a main.m file, but it’s pretty basic:
[sourcecode language=”objc” firstline=”9″]#import <UIKit/UIKit.h>
int main(int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return retVal;
}[/sourcecode]
To go from that code to running your application, a few things happen, mostly in UIApplicationMain
. Its last argument, typically nil
, is a pointer to an NSString
that specifies the class name of the application delegate. Normally, your Info.plist file specifies a nib file (the keys NSMainNibFile
or NSMainNibFile~ipad
), which in turn specifies the application delegate, instantiates it, etc.
So, first, delete the MainWindow.xib file, then all other class files (MyCoolAppDelegate.h and MyCoolAppDelegate.m, for instance). There should be three files left in the iPhone application: your Prefix.pch file (which we could technically do without), main.m, and your Info.plist file.
The Fun Begins
Administrivia: Creating Strings
Since, as a part of the exercise, we don’t want to use Objective-C, I also decided against using any constant NSString
s (@"Hello, World!"
and the like). To create a string, then, we’ll use CFString
s. I created a macro to create one from a C string using ASCII encoding:
[sourcecode language=”objc” firstline=”16″]#define JK_EasyCFString(X)
(CFStringCreateWithCString(kCFAllocatorDefault, (X), kCFStringEncodingASCII))[/sourcecode]
Don’t forget to call CFRelease()
when you’re done with it, though.
Creating a Class
We need to modify the call to UIApplicationMain
to include our application delegate class if we want this app to be anything more than a blank screen. What class name can we give it, though? We’ll need to create a class, and we’ll call it “HardModeAppDelegate
.”
[sourcecode language=”objc” firstline=”41″ highlight=”42″] // Create a class to serve as our application delegate.
Class appDelegate = objc_allocateClassPair(NSClassFromString((id)NSObjectString), "HardModeAppDelegate", 0);[/sourcecode]
The first argument is a toll-free bridged CFString
that specifies NSObject
as the superclass, then we pass in the name of the class as a C string and 0 for the number of extra bytes we want (which is nearly always zero). Now let’s set it up a bit:
[sourcecode language=”objc” firstline=”44″ highlight=”46,49,50,51,52,65″] // Conform to the UIApplciationDelegate protocol.
Protocol *appDelegateProto = NSProtocolFromString((id)UIApplicationDelegateString);
class_addProtocol(appDelegate, appDelegateProto);
// Add methods.
SEL applicationDidFinishLaunchingWithOptions = NSSelectorFromString((id)appDidFinishLaunchingOptionsString);
class_addMethod(appDelegate,
applicationDidFinishLaunchingWithOptions,
(IMP)UIApplicationDelegate_ApplicationDidFinishLaunchingWithOptions,
"v@:@@");
SEL dealloc = NSSelectorFromString((id)deallocString);
class_addMethod(appDelegate,
dealloc,
(IMP)appDelegate_dealloc,
"v@:");
// Add an instance variable for the window.
class_addIvar(appDelegate,
"window",
sizeof(id),
log2(sizeof(id)),
"@");
// Now that we’ve added ivars, we can register the class.
objc_registerClassPair(appDelegate);[/sourcecode]
That code is the same as writing an @interface
block for a new class, setting up ivars, conforming to protocols, and defining instance methods, but done using the runtime calls instead. Methods are actually C functions, but with two arguments prepended to the argument list: id self
and SEL _cmd
. Those arguments are actually passed to every Objective-C method, but usually hidden.
Custom Message Sending
The rest of the application is more of this bootstrapped Objective-C in C, but there are a few wrinkles worth discussing, most notably the use of objc_msgSend
with methods that return other than id
and/or have additional arguments beyond self
and _cmd
. For instance, to call -bounds
on a UIScreen
object, I had to cast the return type of objc_msgSend
to CGRect
:
[sourcecode language=”objc” firstline=”134″ highlight=”135″]CGRect (*msgSendBounds)(id self, SEL _cmd);
msgSendBounds = (CGRect(*)(id, SEL))objc_msgSend_stret;[/sourcecode]
Similarly, when calling -initWithFrame:
on a UIWindow
or UILabel
, I casted it to take in a CGRect
argument:
[sourcecode language=”objc” firstline=”141″ highlight=”142″]id (*msgSendCGRect)(id self, SEL _cmd, CGRect rect);
msgSendCGRect = (id(*)(id, SEL, CGRect))objc_msgSend;[/sourcecode]
Accessing Instance Variables
In my implementation of the HardModeAppDelegate
class’s -dealloc
method, I need to access the window
instance variable to send it a -release
message. Using the Ivar
type and the object_getIvar
function, it becomes easy:
[sourcecode language=”objc” firstline=”196″]Ivar windowIvar = class_getInstanceVariable(self->isa, "window");
id window = object_getIvar(self, windowIvar);[/sourcecode]
So what do we have?
To be clear, this is not writing an iPhone app without using Objective-C, per se. The runtime is still being used, messages are being sent, all that. But it is an illustration of some of the heavy lifting that the runtime does for you. I would caution against adopting this practice for real, shipping apps.