Saturday, April 3, 2010

Converting iPhone Apps to Universal Apps

Well, the NDA has finally lifted, so we can start talking about iPhone SDK 3.2 and the iPad. The logical starting place seemed to be how to convert your existing applications into a "Universal App" that runs natively both on the iPad and the iPhone/iPod touch. Now, a lot of you have likely already had to figure this stuff out so you could get your updated app on the store today, but for those who didn't go the early adopter route, let's take a few minutes to look at the process. It's pretty straightforward but there are a few gotchas.
Note: There are some additional things you should know, so read this post also before tackling your update.

Targeting All Devices


The first thing you have to do is identify that you want to build your existing application as a universal application. For this article, I'm using the Xcode project from OpenGL ES Particle Generator Application, but I'll try to keep the information general. Note: the following step is not needed if you use Xcode's Update Project Target for iPad option talked about here.

Bring up your Project Info window in Xcode by either double-clicking on your project's root node in the Groups & Files pane or selecting Edit Project Settings from the Project menu and then navigate to the Build tab. Now, the change we're about to make needs to be made to all configurations, so make sure that the Configuration popup menu is set to All Configurations, otherwise you'll only make the change on one configuration.

We need to change a setting called Target Device Family, so type Target into the search bar, or just search for that entry manually (it'll be under the Deployment heading). Right now, it should look like this:

Screen shot 2010-04-01 at 10.09.58 AM.png


See how it says iPhone? Yeah, you know what to do. Click on it and change it so it reads iPhone/iPad, like so:

select_target.png


Good! now you're done, right? Most likely, no.

Auditing for Hardcoded Sizes


The next thing you're going to want to do is audit your application to see if you hard-coded the screen size anywhere in your application. You shouldn't have hardcoded those values, but let's face it, we've all done it. A Project Find (⌘⇧F) for 320 and 480 and that should turn up any of those hardcoded values. In the Particle Generator code, I did it in only one place, in code that creates a UIImage of the OpenGL view. The line of code where I did it looks like this:
    CGImageRef imageRef = CGImageCreate(320, 480, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

Usually, the fix for this will be obvious. Instead of hardcoding, you want to pull the width and height from the OpenGL view. The code where I did it actually exists on the GLView class, so I can fix it like so:
    CGImageRef imageRef = CGImageCreate(self.frame.size.width, self.frame.size.height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

But, what if what you've hardcoded is the actual size of the view? Then you need to pull the size from the main screen instead of the view. That's easy enough to do.
    UIScreen *screen = [UIScreen mainScreen];
[myView setFrame:[screen applicationFrame]];


Dealing with Different Window Sizes


Most likely your application's one instance of UIWindow is contained in your MainWindow.xib file that gets loaded automatically. Most likely, that Window is hardcoded to 320x480. Now, you might think that you can just go into Interface Builder and set the autosize attributes for the window and it will get resized for you at launch. You would be wrong. There is no automatic check to make sure your window is the right size.

You have to make sure that the window is the right size for the device you're running on. There are, basically, two ways of doing that. If your application is such that you just need to resize the window and your autosize attributes will take care of making everything look nice, then you can just handle this programmatically in applicationDidFinishLaunching: by setting the window's size to the size of the screen, less the status bar andy any other objects controlled by the iPhone OS (this is known as the Application Frame). Doing this looks almost exactly like setting the size of the view above:
    CGRect  rect = [[UIScreen mainScreen] bounds];
[window setFrame:rect];

Now, this is actually a good approach for the Particles application because it has one full-screen view. However, the iPad and the iPhone are really different devices, and there are several UI components available on the iPad that aren't available (at least yet) on the iPhone, such as split views and pop up views. For many applications, especially complex applications using a lot of UIKit views and controls, you're probably going to want to provide completely different NIB file based on which device on which the code is running.

Info.Plist Device-Specific Entries


The one really important nib file in every iPhone application is, of course, MainWindow.xib, and there has to be a way to tell your application to use a different MainWindow.xib for different devices. In fact, there is. For each key that Info.plist supports, such NSMainNibFile, which is used to specify the name of the application's main nib file, you can now specify device-specific entries. If you provide a device-specific entry for the device the application is currently running on, it will use the device-specific value, otherwise it will just use the normal value.

Device-specific keys are exactly the same as the original or default key except the key name is followed by a tilde (~) and then the name of the device in all-lowercase letters. So, to tell our application to load a different nib file for the iPad, we can add a key called NSMainNibFile~ipad and then specify the name of the nib file to use when launching on an iPad. For the iPhone and iPod touch, it will continue to use the default value, MainWindow.xib, but for the iPad, it will use the nib file you've specified in the new, device-specific key.

You can add a new version of MainWindow.xib to your project by selecting the Resources group and choosing Add New File from the File menu. From the New File Assistant, select User Interface from under the iPhone OS, then select Application XIB, and make sure you select the right device in the Product drop-down.

Screen shot 2010-04-01 at 10.43.29 AM.png


Make sure you remember to connect all the outlets and actions in this new nib to the same outlets and actions you used in the other nib. Remember, only one of the application nibs will be loaded, so there's no conflict.

For any key in the Info.plist file, you can use this same technique to override the default value with a device specific. You could, for example, have the iPhone version start in Portrait and the iPad version start in landscape, like so:
    ...
<key>UIInterfaceOrientation</key>
<string>UIInterfaceOrientationPortrait</string>
<key>UIInterfaceOrientation~ipad</key>
<string>UIInterfaceOrientationLandscapeLeft</string>
...

Programmatically Determining Device


If you have code that needs to vary depending on whether it's running on the iPad or iPhone/iPod touch, Apple has provided a new macro called UI_USER_INTERFACE_IDIOM() that will tell you that. There are currently two values defined, UIUserInterfaceIdiomPhone and UIUserInterfaceIdiomPad, and this macro will return the value that corresponds to the device being run. So, for example, if you needed to push a view controller onto the navigation stack, but wanted a different nib used for the iPad than the iPhone, you might do this:
    MyController *controller = nil;

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
controller = [[MyController alloc] initWithNibName:@"MyiPadNib" bundle:nil];
else
controller = [[MyController alloc] initWithNibName:@"MyiPodNib" bundle:nil];

[self.navigationController pushViewController:controller animated:YES];
[controller release];

If you need finer-grain control, and need to know exactly which device, there's no official, supported way to determine the exact device. The vast majority of the time you you think you need to know the device, you don't actually need to know the device, you just need to know which features are supported. Even though you can find code around the web that will determine the device based on UIDevice, you really shouldn't base your logic on that because such code can be fragile since you don't know what future devices will exist, or what features they will have.

In cases like the Image Picker, Apple provides a way to determine which features are available on your device, such as whether there's a camera, and whether that camera supports video. When Apple hasn't provide a specific check or test, what you can do is use NSClassFromString(), which (as is probably obvious from the name) creates a Class instance based on the name of a class contained in a string. If this returns nil, then you know the class you're asking about isn't available. You can wrap your code that uses classes that aren't available everywhere in these checks and make code that works correctly on all devices, and will continue to do so in the future (for the most part - it's never possible to 100% future-proof code). Here's an example of checking for the existence of the UISplitViewController, which is a new class only available on the iPad:
    Class splitViewController = NSClassFromString(@"UISplitViewController");
if (splitViewController)
{
UISplitViewController* mySplitViewController = [[splitVCClass alloc] init];
// ... configure, use, then release
}

You can do something similar with C functions by checking if the function is NULL For example, one of the frameworks added with iPhone SDK 3.2 is CoreText.framework. If we wanted to use the function CTFontCreateWithName() to create a new font using that framework, we could wrap the logic in an interface idiom check, like above, or we could just check to see if the function we want to use exists by seeing if the symbol CTFontCreateWithName is NULL at runtime, like so:
    if (CTFontCreateWithName != NULL)
CTFontFontRef myFont = CTFontCreateWithName(@"Comic Sans", 14.0, NULL);


Go, Go, Gadget iPad, Go!


Well, that pretty much covers the basics you'll need to convert your existing iPhone apps to Universal Apps. The more complex your app, the more likely you'll want to consider doing separate iPad and iPhone applications. I'll show how to add another target to your Xcode project so you can generate two applications from the same project in a future post. For many apps, however, this should be enough to get you porting away, so port away!



39 comments:

Akuma said...

i tried to convert my app for ipad with your instructions, but whenever i hit "build+run" it switches the simulator back to iphone.
Any Idea why that is happening?

Jeff LaMarche said...

Are you building for 3.2 or 3.1.3? It should automatically use iPhone configuration for <3.13 and iPad for 3.2, but if it doesn't, you can switch the one being used by using the Hardware menu in the simulator.

Akuma said...

Thank you Jeff. That was of cause my mistake ;-)

But it raises another question:
How can my app be still deployable on my iphone 3.1 when i compile for 3.2?
And i got huge problems with my app. e.g. my Tabbarcontroller does not work. No tabs can be selected.
Oh my.
This ipad causes much work :-/

Another topic: thanks for your great books. The app i created while and after reading "beginning iphone development" was lately featured in the german iphone commercial. Would not have been possible without your work!

sean said...

great post. love having all the details in one place. thanks for sharing!

sean808080

Donald W. Miller, Jr. said...

I guess you'll have to spell it out for me. Why bother with a Universal app in the first place for existing apps? As you said if there's a lot of difference - build two separate apps for each device. My apps were never really designed to be used on a larger screen and really don't need the space (e.g. utilities). Since these iPhone apps work on the iPad (in iPhone-size), why bother? And then this is all gratis for existing customers? Humor me ;)

Vittorio said...

great post, it sums up a lot of steps to create universal apps

however these universal (iphone/ipad) apps, how can they be compiled by different sdks if the sources use APIs present only in 3.2?
even the way you suggested to detect whether we're on iphone or on ipad (UIUserInterfaceIdiomPad) only works when building for 3.2, so when i try to compile my program from 3.0 it fails

am i missing something? thanks

Jeff LaMarche said...

Vittorio and Akuma:

See today's post where I talk about those issues.

Donald:

No offense, but I don't really care which of the two options a developer opts for. There are arguments for both doing Universal apps and for doing standalone, separate apps, and I don't have a dog in the race either way.

Donald W. Miller, Jr. said...

No offense taken. Just thought I missed something entirely. Thanks for this and all of your posts (BTW, I have ALL your books, in all editions).

Greg said...

Jeff,

I'm running into a rather odd problem when I compile to 3.2. My iPhone app automatically comes up in iPad mode in the simulator. But when I switch to iPhone in the hardware menu, the app exits and the iPhone simulator goes to the home screen, and my app disappears. When I restart the app, the same process starts all over again.

Thoughts?

vibhor goyal said...

I am having the same issue as Greg. Please help. Thanks!

Noel said...

I can get past by using "Base SDK" to compile

noblemaster said...

That's the solution for UI_USER_INTERFACE_IDIOM()...


#ifndef __IPHONE_3_2 // if iPhoneOS is 3.2 or greater then __IPHONE_3_2 will be defined
typedef enum { // provided by noblemaster ]:-|
UIUserInterfaceIdiomPhone, // iPhone and iPod touch style UI
UIUserInterfaceIdiomPad, // iPad style UI
} UIUserInterfaceIdiom;
#define UI_USER_INTERFACE_IDIOM() (([[UIDevice currentDevice].model rangeOfString:@"iPad"].location != NSNotFound) ? UIUserInterfaceIdiomPad : UIUserInterfaceIdiomPhone)
#endif // ifndef __IPHONE_3_2

noblemaster said...

sorry for the bad formatting...

CodeForever said...

CGRect rect = [[UIScreen mainScreen] bounds];
[window setFrame:rect];

This doesn't work if you force landscape , it still reports sizes as if they were portrait

joe said...

I'm having the same issue as Greg and Vibhor: my app always launches in the iPad simulator. Switching to iPhone using the Hardware menu is no help, since my app isn't over there, it's in the iPad simulator. How do I test my app in the iPhone simulator?

(And note: this is happening even though my app isn't yet configured for iPad! It's launching as an iPhone app, with the standard "1X/2X" mode button.)

joe said...

Never mind. I see (from your later post) that this is just how the simulator works when you use the 3.2 SDK. To test in a simulated iPhone, you have to set the active target to 3.1.3.

Mike Weller said...

Your [window setFrame:rect]; thing just saved me, thanks. The side effect of not doing this was strange auto-rotate animations showing an iphone-sized box moving around, and touch events outside that box not being detected.

sukumar said...

Thats very useful picture information about iphone detail. keep it up more post.iPhone Application Development

ACS said...

hello great post,
i am trying to do this however after upgrading my target i get only the top screen of the ipad to work with clicking adn the bottom half doesnt respond to any touches in the simulator. does anyone have any solution to this?

charles said...

Hey im having a problem getting my iPhone app to be uploaded to the app store after i chose to upgrade current target for ipad. It keeps giving me this error message when i try to upload it using the application loader. The Bundle is invalid. An application targeting the iPhone device family may not require a iPhone OS deployment target of 3.2,which is an ipad-only OS. NOw ilistened to the instructions in the 3.2 release and it says to do a uiniversal application ill need to have my base sdk in 3.2 which i do and my deployment OS as 3.1.3 or lower im using 3.1.3, now i dont understand why im getting this error and its ticking me off. Any suggestions?

charles said...

Actually i figured out why i was getting my previous error. What apple dosent tell you is that when you choose base sdk as 3.2 and the deployment type to 3.1.3, is that their is another parameter you have to change in the project setting general tab. You have to change project compatibility to 3.1 In order for the program to run on an iPhone using sdk 3.1.3, 4 and the iPad.

June said...

This is great! If only more apps were made for android and not for apple, which is not very friendly to the dev community. This tut helps solve this problem, which I think is the first step toward a dev community that is platform agnostic.

June said...

The reason I favor platform agnostic app development is that there is increased competition. This leads to better apps, whether they are iphone apps, android apps, or what have you.

Sfasquel said...

Excelent post saved me a bunch of time getting it converted over.

Thanks for sharing.

Muhammad Arsalan said...
This comment has been removed by the author.
Muhammad Arsalan said...

i have same problem as AKuma mentioned

Melvin said...

Using an universal application for all these three - iPhone, iPad and iPod is a great thing. The code to convert and application into universal application can be very much helpful. Nowadays people are using iPhone, iPad and iPod together and they would like what you have shown here as that can save thier time.

iphone application development

Mobile Application Development said...

Hi, Good work!!! I am an iPhone developer. I was converted my many iPhone Application into iPad Application. Next time I’ll use your code. Hope this code will help me to create universal application that will runs natively both on the iPad and the iPhone.

iPad Application Development

ipad Application Development said...

Nice one. They create a niche market for themselves and they have a niche line of followers who always stick to Apple products.

http://ipadapplication-development.com

Jamie Chapman said...

Great post, thanks Jeff!

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels

ipad Application Development said...

What a tremendous description for the ipad apps. You can work on developing different kinds of productivity platforms.

ipad application development

Fábio said...

Great post! But I would like to know to do the inverse, ie, create an universal app from an iPad application that uses the split view template.

Thanks!

Hire iphone developer said...

Hello,
Outsourcing companies have talent that can professionally perform your project of game development. They have experienced game developers who know each and every tricks and tweaks of the game development market.

iphone developer

Paddy said...

iPhone the device that started the trend for Application store iPhone apps It’s a whole new aspect to mobile technology which examines the extra things your mobile can do & It’s all been done by brilliant iPhone 3GS & iPhone 4 Professionals.

launch-mailinator-com said...

OpenID noblemaster said...

"That's the solution for UI_USER_INTERFACE_IDIOM()..."

You don't need this. Just compile with Base SDK >= 3.2. UI_USER_INTERFACE_IDIOM() will work fine on devices < 3.2

T.W. said...

Wow, I honestly didn't even know that you could publish the app so that it was universal. This is a fantastic guide! I'm going to cue up your blog so that I can read more later. I have a page that you might like that I set up for total beginners such as myself..
http://www.squidoo.com/makeiphoneapplication

Mark Andrews said...

Jeff, I'm working on turning an iPad app into an iPhone app. Because of so many objects in the screen I am using the method of having multiple .xib files for those viewControllers. Currently I am working on xCode 4.0 with the new Interface built in. My question is this. If I duplicate the .xib file created for the iPad, can I somehow turn that into an iPhone .xib. I've tried to copy it finder level. Bring it into xcode with new name. Some of the view sizes aren't changeable in interface builder. They are grayed out. So i create a new view and copy my elements over. But when i set the status bar to black it resizes the window to ipad size. How can I turn it into an iPhone .xib?

ps. I know this is an old post but thought id try.
Ya, I can just recreate the whole thing from scratch and copy paste the elements but then I have to do all the linking also. Hoping there is a way to do this so it's just a little easier.

thanks, Mark

David Cásseres said...

In Xcode 4, the Attributes Inspector for UIWindow has a checkbox labeled "Full Screen at Launch." Check that, and your window will automatically be sized for whatever screen it opens on. Pretty nice.