Tuesday, April 20, 2010

More Thoughts on iPad Development Decisions

I currently have two iPad apps available in iTunes. One, Artificial Life HD, is an iPad specific app which is a modified version of my Artificial Life iPhone app. I initially struggled with the decision of whether to release the iPad version of the app as a universal app, compatible with both iPhone and iPad, or as two separate apps. After doing some initial work in pursuing a universal app design, I decided it would be better to have an iPad-specific version.

There were mainly 3 reasons for this decision.

First, the iPhone app is currently slightly under 20 meg in size. 20 meg is an important number for iPhone apps. If an app is larger than 20 meg, Apple will not allow the app to downloaded via the cellular network. This has the potential for lost sales. Adding support for the iPad required adding higher resolution artwork and some iPad-specific code additions which made it very likely that the app would be larger than 20 meg.

Second, due to some early decisions I had made in the design of the the iPhone app, it was going to be very tedious to make the app run-time compatible with both iPad and iPhone. It was definitely possible, but a lot of time could be saved by making the iPad changes compile-time rather than run-time.

Third, while both the iPad and iPhone versions are nearly identical in functionality right now, I hope to be adding features to the iPad version which take better advantage of the larger screen area. Having the apps as two separate and distinct versions in the app store made the most sense.

My second iPad app is a universal app - Tunemark Radio. Tunemark Radio is a free streaming radio player which supports the full SHOUTcast directory as well as adding custom streams via pls, m3u or other custom URL formats. It supports both mp3 and AAC+ audio formats. It also has a few unique features such as support for custom wall-paper and a built-in fullscreen visualizer which reacts to the music. In the case of this app, a universal app made the most sense. The app binary is quite small (about 1 meg) and there will be little change in functionality between the two apps. The iPad version required a new layout, which meant new iPad specific view controllers, but the code differences were minor and were easy to handle during run-time.

In addition to becoming more familiar with the iPad vs. iPhone differences while adding iPad support for these apps, I learned a few important details during the app submission process.

1) When using a UIPopoverController, it is very important that you test that the popover continues to point to the correct section of the iPad screen as the screen is rotated to various orientations. This means testing rotating the screen while the popover is being displayed, not just testing the popover’s initial appearance in each orientation. I had the Artificial Life HD app rejected because one of my popovers did not redisplay itself with the arrow pointing to the proper button after the screen was rotated. I had never thought to test that behavior and has falsely assumed it would be handled automatically via the iPhone SDK internal logic. This is not the case.

In order to have your popover redisplayed properly when the screen is rotated, you must first save all the popover attributes to some state variables. This includes saving the sender which initiated the popover, the containing view, the popover arrow position, and a pointer to the popover itself. Here’s a snippet of code displaying a popover and saving some of the creation details:

                Class classPopoverController = NSClassFromString(@"UIPopoverController");
                
id aPopover = [[classPopoverController alloc] initWithContentViewController:navController];
                
activePopover = aPopover;
                
activePopoverSender = sender;
                
activePopoverView = sidebarView;        
                
activePopoverArrowDirection = UIPopoverArrowDirectionLeft;
                [aPopover
presentPopoverFromRect:[activePopoverSender frame] inView:activePopoverView permittedArrowDirections:activePopoverArrowDirection animated:YES];

I suggest saving the sender rather than the sender’s frame because when the screen orientation changes, the sender’s frame could change position and size, so you want to be able to get the current frame size and position, not the size and position when the popover was first displayed.

Then, you need to add some code in the view controller’s didRotateFromInterfaceOrientation to check for the presence of a popover, and if one is active, redisplay it. Here’s an example:

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{        
        
if (activePopover){
                [
activePopover presentPopoverFromRect:[activePopoverSender frame] inView:activePopoverView permittedArrowDirections:activePopoverArrowDirection animated:NO];
        }
}

Even though the popover is already being displayed, it’s OK to call presentPopoverFromRect: again. Doing this has the effect of simply redisplaying the existing popover at the proper new orientation. If you don’t do this, sometimes the popover will rotate properly, but in most cases it will not and if Apple notices this, your app will be rejected.

2) On the iPad, Apple strongly suggests supporting all orientations. If you do support all orientations, then it is critical that all views display at the proper current orientation.

This seems like an obvious point, however if you are converting an app from an iPhone-specific project and you have lots of view controllers, it’s easy to overlook adjusting a view controller so it supports all the orientations. As a result, you need to make sure you test every single view at every single orientation while testing the app. It may not be immediately obvious if some buried menu item is only displaying at one orientation. It’s an easy enough fix if you do discover something comes out wrong - just find the view controller responsible and make sure shouldAutorotateToInterfaceOrientation returns the proper value for iPad.

If you are doing this in a universal app and the iPhone version only supports one orientation, here’s an easy way to handle it:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
        if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad){
                
return (interfaceOrientation == UIInterfaceOrientationPortrait);
        }
else{
                
return YES;
        }
}

3) If using a UIPicker (such as date picker) on iPad, it must appear within a UIPopoverController. This is mentioned in the HIG, but it's just one sentence and is easy to overlook. If you don't display a picker within a popover, Apple will reject your iPad app. I'm not sure why this is a requirement, but rules are rules, as they say.

These are some fairly simple examples, but may be easy to overlook the first time you create an iPad app from an existing iPhone project. Hopefully they can help someone else avoid time lost in resubmitting an app.

All content copyright © 2009  Brian Stormont, unless otherwise noted.   All rights reserved.