Developing a Universal iPhone / iPad App

I’m interested in finding the cleanest, simplest way of developing universal apps – those that run seamlessly on all generations of the iPhone, iPod touch and iPad.

Much seems to have been written on this already. What I’m presenting here is what I found, after a few hours of googling and experimenting… that just works. I’m still interested in finding something more elegant. Let me know what you think.

The aim of all this

We know already that it is possible to create one app that runs on iPhones, iPod touches and iPads. The question is, how do we design such an app so that it makes the best use of the different screen sizes available? Perhaps we might want to expose certain additional features when the app is run on an iPad, which we simply won’t have room for on the much smaller screen of an iPhone.

Getting started

I’m assuming some basic knowledge on creating iPhone or iPad apps here.

Xcode 4 makes it very easy to get started, by automatically generating the basic classes and UI elements required in most universal apps.

Start by either opening up Xcode and choosing “New project” from the opening screen, or if you already have it open, use: File | New | New Project…

Choosing an app template in Xcode

Choosing an app template

I usually start by choosing the basic, Window-based Application template, regardless of the type of app I’m designing, and then add on any additional UI features afterwards by myself. Click the Next button to continue.

Choosing options for the new app

Choosing options for the new app

After entering the product name for your new app, make sure that the Universal device family type is selected. You can choose to include support for Core Data if your app needs it. Unit tests are always needed, of course! :)

After clicking the Next button you will be asked to choose a folder to store your new app project in, and then the basic framework of your app will be generated automatically.

Universal app framework generated by Xcode

Universal app framework generated by Xcode

In this example, you can see that Xcode has automatically generated:

For all users, regardless of device type, common:

  • Application delegate class (MyUniversalAppAppDelegate.m & MyUniversalAppAppDelegate.h)
  • An empty data model (if you included Core Data support)

For iPhone users:

  • An application delegate class that inherits from the common one (MyUniversalAppAppDelegate_iPhone.m & MyUniversalAppAppDelegate_iPhone.h)
  • A main window nib file, sized to suite the iPhone

For iPad users:

  • Another application delegate class that inherits from the common one (MyUniversalAppAppDelegate_iPad.m & MyUniversalAppAppDelegate_iPad.h)
  • A main window nib file, sized to suite the iPad

You can even go right ahead now and test this simple app on your iPhone or iPad, or both. Just connect the device as usual, select it from the drop-down list of target devices (here, you’ll have noticed I have my iPhone selected) and click on the Run button.

But of course, this app doesn’t really do anything useful at this point. Depending on whether you are running it on your iPhone or iPad, you should see one of these two messages in black text on a white background, in the middle of your device’s screen:

My Universal App on iPhone

or

My Universal App on iPad

If you take a look at each of the nib files that were generated (MainWindow_iPhone.xib and MainWindow_iPad.xib), you’ll notice that these messages are simply static text on label elements. You’ll probably want to delete them before continuing, unless you happen to find them particularly entertaining!

How does this work?

Somehow, the app seems to know which type of device it is running on. But how?

The answer lies not within the source code, but simply in the way that it is configured. Looking at the project summary, you can see how the app chooses to run the application delegate class, depending on which device is in use.

Main user interface setting for iPhone users

Main user interface setting for iPhone users

iPhone and iPod touch users are presented with the UI defined in the MainWindow_iPhone xib. This xib includes a link to the MyUniversalAppAppDelegate_iPhone class, so this is where iPhone and iPod touch users start when the app runs.

Main user interface setting for iPad users

Main user interface setting for iPad users

The setting for iPad users is found further down this page. Here, we can see that they are presented with the UI defined in the MainWindow_iPad xib instead. Not only is this sized to suite the iPad’s larger screen, but it is also linked to the application delegate class that was defined for iPad users, MyUniversalAppAppDelegate_iPad. This allows not only the UI design, but also the behavior of the app to be customized where necessary for iPad users.

Making it a bit more useful

As we’ve just seen, generating a basic universal app is very simple with Xcode. But in practice, we usually want to do a bit more than just display static text on the screen. In most cases, we’ll want to add a view to the main window and a view controller class to go with it. This is really where Xcode leaves us and we need to start thinking.

I found that the cleanest way to do this, for most universal apps, is to simply follow the convention that Apple have already given us here in this auto-generated code:

  • Code and UI nibs that are only for iPhone & iPod users go in the iPhone folder
  • Code and UI nibs that are only for iPad users go in the iPad folder
  • Code and UI nibs that are common to all device types go somewhere else, such as in the main folder underneath the iPhone and iPad folders

We probably want to add a view controller for the main view in the app, so let’s start by adding these files to the common code.

  1. Right-click on the main folder for the app (MyUniversalApp in this example) and choose “New File…” from the menu
  2. We are creating a view controller, so select the UIViewController subclass item from the list:
  3. Click Next to continue
  4. Leave the subclass as its default of “UIViewController” and ensure that neither the “Targeted for iPad” nor the “With XIB for user interface” options are checked:
  5. Save the new class using the name “MainViewController.m”. Xcode automatically chooses the project’s root folder by default.
  6. Click on the Save button.

The interface and implementation files for this class will then be generated and inserted into your project automatically.

Although our app will have features that are common to both the iPhone and iPad, we also need to take care of the different UI features that it will have, depending on which device it is running on. We’ll address this by creating separate view nibs and view controllers for each device type.

The iPhone View

The iPhone view will consist of a 320 x 480 pixel nib, upon which we will add our controls for the app. The controller from this view will be a class that inherits from the MainViewController class we created above.

  1. Right-click on the iPhone folder and choose New File… from the menu.
  2. Once again, select the UIViewController subclass item from the list:

    Choosing an app template in Xcode

    Choosing an app template

  3. Click Next, and then you will be prompted for the options:
    Selecting options for the iPhone view controller

    Selecting options for the iPhone view controller

    This time, remember to replace the default subclass of UIViewController with MainViewController. Check the box for “With XIB for user interface”.

  4. Save the new class with the name MainViewController_ipad.m

    Saving the new view and view controller class

    Saving the new view and view controller class

    This time, make sure you select the iPhone folder, which Xcode automatically created under the root folder for your project.

  5. Click on the Save button, and three new files will be generated under your project’s iPhone folder:

    Files generated for the iPhone view and view controller class

    Files generated for the iPhone view and view controller class

  6. At this stage, I usually rename the nib file. It is not really a controller, so renaming it to MainView_iPhone.xib makes more sense to me.

The iPad View

You guessed it. Just repeat these steps for the iPad view, substituting “_iPhone” with “_iPad” in the file names you choose. Also remember to select the “Targeted for iPad” option:

Selecting view and view controller options for the iPad

Selecting view and view controller options for the iPad

Wiring it all together

We have created windows and views for each device type, but if you already tried adding some UI components to your views, you’ll have noticed that they don’t yet appear when you start up your app.

The reason for this is that we still need to add some code to attach the views to their relevant windows, depending on which device type is being used.

I chose to initialize the iPhone main view controller from the iPhone version of the app delegate, and the iPad main view controller from its app delegate. This seemed to be the cleanest route, as it avoided the need to have any if(IS_IPHONE) { do_iphone_ui_stuf(); } else { do_ipad_ui_stuff() } type logic in the code.

In MyUniversalAppAppDelegate.h, you can see that I have added a property for the app’s MainViewController object:

//
//  MyUniversalAppAppDelegate.h
//  MyUniversalApp
//
//  Created by Graham Daley on 9/8/11.
//  Public domain.
//

#import <UIKit/UIKit.h>
#import "MainViewController.h"

@interface MyUniversalAppAppDelegate : NSObject <UIApplicationDelegate>

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) MainViewController *mainViewController;
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

@end

This object is synthesized and released in the main implementation of MyUniversalAppDelegate.m, but actually not initialized here. Instead, it is the subclasses, MyUniversalAppDelegate_iPhone and MyUniversalAppDelegate_iPad that initialize this object depending on the subclass of MainViewController that is required.

In iPhone/MyUniversalAppAppDelegate_iPhone.m, we override the didFinishLaunchingWithOptions method from the main app delegate, and use it to initialize the iPhone version of the main view controller:

//
//  MyUniversalAppAppDelegate_iPhone.m
//  MyUniversalApp
//
//  Created by Graham Daley on 9/8/11.
//  Public domain.
//

#import "MyUniversalAppAppDelegate_iPhone.h"
#import "MainViewController_iPhone.h"

@implementation MyUniversalAppAppDelegate_iPhone

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"MyUniversalAppAppDelegate_iPhone:didFinishLaunchingWithOptions");

    // Override point for customization after application launch.
    // Add the main view controller's view to the window.
    [self setMainViewController:[[MainViewController_iPhone alloc] initWithNibName:@"MainView_iPhone" bundle:nil]];
    [[self window] setRootViewController:[self mainViewController]];

    return YES;
}

@end

and in iPad/MyUniversalAppDelegate_iPad.m, we initialize the view controller used when running on the iPad:

//
//  MyUniversalAppAppDelegate_iPad.m
//  MyUniversalApp
//
//  Created by Graham Daley on 9/8/11.
//  Public domain.
//

#import "MyUniversalAppAppDelegate_iPad.h"
#import "MainViewController_iPad.h"

@implementation MyUniversalAppAppDelegate_iPad

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"MyUniversalAppAppDelegate_iPad:didFinishLaunchingWithOptions");

    // Override point for customization after application launch.
    // Add the main view controller's view to the window.
    [self setMainViewController:[[MainViewController_iPad alloc] initWithNibName:@"MainView_iPad" bundle:nil]];
    [[self window] setRootViewController:[self mainViewController]];

    return YES;
}

@end

You can then start adding your UI elements to each view, and link them to methods or properties in their corresponding view controllers. Avoid writing duplicate code, by putting common properties and functionality in the parent, MainViewController class.

And there you have it

Let me know what you think about this article. I’d be glad to answer your questions. Do you have a more elegant method of doing this?

12 Comments

  1. Good post.

    I’d like to share my experience. If this app’s iPhone version is similar to iPad version, what I do is to implement iPhone version first. And then copy MainView-iPhone.xib and rename it to MainView-iPad.xib. Change the size of the views in MainView-iPad.xib.

    Create this view controller like this:
    if (!g_isiPad) // You need to check if it is iPad first
    … = [[MainViewController alloc] initWithNibName:@”MainView-iPhone” bundle:nil];
    else
    … = [[MainViewController alloc] initWithNibName:@”MainView-iPad” bundle:nil];

    So you have two .XIB files, but all logic exists in one .M file. It is easy to maintain.

    In the view controller .M file, handle orientations:
    // Override to allow orientations other than the default portrait orientation.
    – (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    if (!g_isiPad) {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
    }
    else {
    return YES;
    }
    }

    Hope it helps.
    (Follow me on twitter: vinceyuan )

  2. A lot depends on the app itself, and the features you need. Go as far back as you can in the iOS version level to support the most devices out there. Believe it or not, I think that most people don’t upgrade iOS, especially on iPods. You have to look at each and every API call you make, and look at what version of the iOS it was introduced (or deprecated) in.

    Then, there’s the app functionality itself. For example, let’s say you design for a small screen device. This means you make a MVC with a certain number of controls and IBAction methods on it. Then, you want the same Controller to function on an iPad, where you develop a separate View with more controls on it. Suddenly, what you used to have to do in multiple MVC’s, you now can do in one.

    What I opted to do, and works well in my case, was to have the main screen’s Controller incorporate the same functionality as the satellite screens for small devices. This means copying/pasting code, or I suppose there may be a better way to do that by declaring a helper class and using that in both sets of Controllers. Either way, with a little effort the app becomes seamless to the user – it just works on all devices, one app, and by the way there is only one app you have to submit to iTunes and manage provisioning for.

  3. Hello Graham – I liked your post and your approach and would like to make it work. First, I noticed that in your didFinishLaunchingWithOptions override in the @implementation MyUniversalAppAppDelegate_iPad, [MainViewController_iPhone alloc] was used rather than the _iPad controller.

    That aside, I tried the version for the iPhone, and when it goes to initWithNibName:@”MainView-iPhone”, I get this error:

    *** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Could not load NIB in bundle: ‘NSBundle (loaded)’ with name ‘MainView-iPhone”

    Any ideas? I am using XCode Version 4.0.2. I do notice that XCode has but the two new view-XIBs in a different directory than the window-XIB: it put both the iPhone & iPad view-XIBs in the topmost directory, while for example MainWindow_iPhone.xib went under …/iPhone/en.lproj. Does this have anything to do with it? If so, how did that happen and what to do?

    Thanks in advance.

  4. @Karl – Sorry, there were some typos (corrected now). The nib name should have underscores instead of hyphens, e.g.:

    initWithNibName:@”MainView_iPhone”

    Thanks for pointing this out. Let me know if you still have problems getting it to work.

  5. Thank you very much for your detailed article. This is exactly I am looking

  6. please suggest me how to implement universal app using macros how to detect device using macros .

  7. sandeep saurabh

    15/03/2012 at 4:13 pm

    is there any way to detect device using macros

  8. @sandeep – sorry, what kind of macros are you referring to? You might find this useful: http://www.cocos2d-iphone.org/forum/topic/8107 . Scroll down and see the comment from user ‘patgoto’.

  9. Graham, Thanks a lot for this blog post.

    I’m still a beginner, and I’m approaching a universal app for the first time. I like the idea of creating a MainViewController, and then MainViewController_iPhone & MainViewController_iPad that subclass it.

    Sometimes the docs overwhelm, so it’s nice to read this really simple example showing how to organize my classes.

    Ben

  10. Thank Graham,
    I am a beginner for and i am making an existing application.Your this blog post is good for making an application universal,if we are starting the app development fist time.
    So if you have any link or suggestion regarding my Query.
    Making existing application universal.
    Thanks in Advance…
    Sanjay

  11. You share interesting things here. I think that your blog can go viral easily, but you must give it initial boost and i
    know how to do it, just type in google for – wcnu traffic increase

  12. I read a lot of interesting content here. Probably you spend a lot of time
    writing, i know how to save you a lot of work, there is an online tool that creates high quality, google friendly posts in minutes,
    just search in google – laranitas free content source

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2014 Graham's Blog

Theme by Anders NorenUp ↑