BuySellAds.com

Until Dec 3rd, 44% off all Manning books, including Barcodes with iOS! Promo code: mobicftw
Our DNA is written in Objective-C
Jump

Revisited

The last update for SpeakerClock came out in March 2012, about time that I had a look at a few issues that users have reported and maybe add some fancy new stuff.

With every old app of mine I am looking at I find that I was still using the old paradigm for creating a window and root view controller. Though ever since iOS 6 and the iPhone 5 this has become outmoded.

In this blog post I will have a look at some old code and how it can be properly modernized. Maybe I can give you a slight nudge with it to do the same on some of your own old projects.

The first thing I noticed when opening up the SpeakerClock Xcode project was an offer to create a tall iPhone 5 launch image. This is only a black image, but it activates the Giraffe-Mode. So I said yes.

Then when I tried to launch the app I noticed that the graphics are no longer centered and also device rotations ceased to work. Also I got this message in the console which points us in the right direction.

2012-11-19 10:51:23.528 SpeakerClock[2876:c07] Application windows are expected to have 
a root view controller at the end of application launch

With the introduction of iOS 5 Apple established the guideline that besides a hierarchy of views your app is also supposed to have a hierarchy of view controllers. As of iOS 6 this guideline has become a rule. If you want device rotations to be passed onto your view controllers then the app’s window must have its rootViewController set and all other view controllers must be children of that. Similar to how all your visible views form a tree hat has its root in the app’s window.

It is very likely that you will see the same problem because the code that’s in need to be modernized was part of the Xcode project templates for the longest time. You’ll have this code in your app delegate:

- (void)applicationDidFinishLaunching:(UIApplication *)application 
{
    [window addSubview:[mainViewController view]];
    [window makeKeyAndVisible];
}

There is also a MainWindow.xib that creates an instance of a UIWindow, plugged into the app delegate’s window property. As well as a main UIViewController that is linked with the mainViewController of the app delegate.

Digging a bit deeper we realized how the whole thing works. UIApplicationMain in your main.m checks the info.plist for the NSMainNibFile. It loads that with the UIApplication as it’s File’s Owner. It also creates an instance of the app delegate in there and fills in the properties while decoding MainWindow.xib.

To support the iPad there are also MainWindow-iPad.xib and MainView-iPad.xib which in turn are automatically loaded by the NIB loader via the NSMainNibFile~ipad in the info.plist.

Then in the app delegate the above code grabs the mainViewController’s view and adds it to the window. Finally the makeKeyAndVisible on the window makes it visible and accept “key” input.

This code does not set the rootViewController and therefore is illegal as of iOS 6. And the proper punishment for such crimes is to be denied rotations. So let’s repent and fix that. Apple has update the project templates to use a different method.

For a brief moment we are pondering whether this app would benefit from being changed to use story boards. But it doesn’t because 95% of the UI is custom buttons to achieve the LED look. So we’ll be satisfied if we can simply modernize this view/view-controller hierarchy.

Modernize

Firstly we want to get rid of creating the UIWindow in a XIB file for the simple reason that this doesn’t know about the actual resolution of the device. There is no other way than to get the bounds of the main screen and use these while creating the main window in code, in our app delegate.

Secondly if you look at the above screenshot from Interface Builder you can see that the MainWindow.xib doesn’t really give us any benefit for laying out the UI elements. All it does is create an instance of the app delegate class and an instance of the main view controller. The latter is loading its contents from a MainView.xib anyway. So we can change these instantiations to code.

Thirdly I prefer to have the XIB that belongs to a given view controller to have the same name as it. Technically a view controller will always load its view from the NIB and never itself, but for clarity it is smarter to have them named the same.

Automate App Delegate Creation

Now, if we don’t have the app delegate instance no longer come from MainWindow.xib (because we’ll remove that from the project), where will it actually be created?

Well, the UIApplication instance does that for us, provided we inform it about what class should serve as the app delegate. Behold an updated main.m from Apple’s template:

int main(int argc, char *argv[])
{
	@autoreleasepool {
	    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
	}
}

(This reminds me of another thing, this app needs to be converted to ARC as well.) Check out the fourth parameter of this C-function which Apple explains so well in the UIApplication.h header:

// If nil is specified for principalClassName, the value for NSPrincipalClass from the Info.plist is used. If there is no
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);

ARGC and ARGV are needed to deal with Unix-command line parameters. Since we usually don’t have a command line on iOS we don’t care about these. We are perfectly happy with UIApplication used as the principal class name. But very importantly we want to specify the delegateClassName so that the proper delegate class is used, “instantiated using init”.

This parameter is an NSString, so we can either just hard-code it with a string literal or do what the template does by getting the class name via NSStringFromClass. Matter of taste. I prefer the hard-code because this way I don’t need to import the class header in main.m.

Having set the class name for the app delegate we don’t need the instance creation for it in the MainWindow XIBs.

Create Root View Controller in Code

Next we need convert to creating our root view controller to code. So first lets get the naming right.

MainView.xib becomes MainViewController~iPhone.xib. MainView-iPad.xib turns into MainViewController~iPad.xib. This naming makes it clear for which platform each NIB is for. Unfortunately the NIB loader is not yet able to magically determine the right file to load by the tilde suffix, as it can for images, but why not already adopt the correct naming convention?

Both MainWindow NIBs go into the trash. So do their two respective entries from info.plist. When writing this article I had forgotten about this and so I was still getting the above mentioned console error message because somehow my app was still able to find a left-over copy of the XIB.

MainViewController gets a smart init method to decide to load the correct NIB for the platform it is running on.

- (id)init
{
	NSString *nibName;
	if (UI_USER_INTERFACE_IDIOM()	== UIUserInterfaceIdiomPad)
	{
		nibName = @"MainViewController~iPad";
	}
	else
	{
		nibName = @"MainViewController~iPhone";
	}
 
	self = [super initWithNibName:nibName bundle:nil]
	if (self)
	{
		self.wantsFullScreenLayout = YES;
	}
	return self;
}

And the updated app delegate looks like this. We haven’t flipped the ARC switch to on yet, but that’s not within the scope of this article.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	// create properly sized window
	self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 
	// create instance of root VC and assign to window
	MainViewController *vc = [[MainViewController alloc] init];
	self.window.rootViewController = vc;
	[vc release];
 
	[self.window makeKeyAndVisible];
 
	return YES;
}

Note that we use the more modern didFinishLaunchingWithOptions version. If you add this while the older one without options is still in place then the older one will take precedence.

Alive! Again!

At this point the patient awakes and is still alive. The next step is now to replace some alignment code with one that calculates the offset such that my LED digits are also centered on iPhone 5. And probably later today I’ll convert the whole thing to ARC because all these releases make me sick to my stomach now that I’ve been on ARC for quite some time now.

It is quite interesting to modernize old code and to catch up on the progress that Apple has made since you last touched this code. I also find it quite illuminating to think of the reasons why Apple has their project templates in Xcode a certain way.

Now with iOS 6 becoming more and more universally adopted by iOS device owners you have little reason to not update your code as explained in this article.


Categories: Recipes

%d bloggers like this: