Chapter 11
Creating a Universal App

WHAT YOU WILL LEARN IN THIS CHAPTER:

 
  • Creating an iPad Storyboard
  • Supporting Rotation Using Auto Layout
  • Implementing Popovers

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

You can find the wrox.com code downloads for this chapter at www.wrox.com/go/begiosprogramming on the Download Code tab. The code is in the chapter 11 download and individually named according to the names throughout the chapter.

The Bands app you have created so far is designed to run on an iPhone or an iPod Touch. You can also run the app on an iPad using the iPads compatibility mode. Compatibility mode displays the app on an iPad as though it is running on an iPhone, even using the same dimensions. You can use the 2x button in compatibility mode to double the size of the app so that it uses the full screen, but apps tend to look pixilated when doing this, deteriorating the user experience. iPad users typically choose to run apps that were designed for iPad. Developers have the option to create a separate iPad app or they can create a universal app.

The guts of a typical iOS app will be the same no matter what device they run on. Therefore, universal apps can share much of the code between their iPhone and iPad implementations. The big difference is the user interface. Universal apps are a single build that contains user interfaces and code for both iPhone/iPod touch and iPads. iPads are obviously bigger and thus have more screen real estate. Designers can use that space to create a user interface that is much different from their iPhone design.

The Bands app, as discussed in Chapter 1, “Building a Real-World iOS App: Bands,” is not meant to be an example of a well-designed user interface. Instead it is meant to be a tool to teach you how you code an iOS app. The last part of that is learning how to create an iPad implementation. The user interface you will implement for iPad is almost identical to the iPhone version. There are user interactions that need to be changed to comply with Apple’s Human Interface Guidelines. The other major difference is it will support rotation. To start, you need a new Storyboard with new scenes designed for iPad.

TRANSITIONING TO A UNIVERSAL APP

The first step in creating a universal app is to transition from an iPhone project to a universal project. This can be done in the project settings with just a few clicks. The other aspect is creating a new user interface for iPad. Instead of adding to the iPhone Storyboard, you need to create a new Storyboard designed for iPad. This is all straightforward, as you see in the next Try It Out.

TRY IT OUT: Adding an iPad Storyboard
 
  1. From the Xcode menu select File ⇒ New ⇒ File.
  2. Select the User Interface section on the left side of the dialog, select Storyboard, and click next.
  3. On the next screen, select iPad as the Device Family.
  4. Name the new Storyboard Main-iPad, and save it in the Base.lproj directory with the Main.storyboard file.
  5. Select the Main-iPad.storyboard from the Project Navigator if it’s not already selected.
  6. Drag a new Navigation Controller from the Object library onto the new Storyboard.
  7. Select the Project from the Project Navigator.
  8. In the Deployment Info section, change the Devices setting to Universal.
  9. Select Don’t Copy in the dialog that appears after.
  10. Under the Devices setting you now see iPhone and iPad. Select iPad.
  11. Change the Main Interface to the Main-iPad.storyboard.
  12. Check all the Device Orientation options.
  13. Run the app in the iPad simulator. You now see an empty UITableView, as shown in Figure 11-1.
    阅读 ‧ 电子书库
    FIGURE 11-1
How It Works
The first thing you did was to create a new Storyboard targeted for iPad. When new view controllers are added to this storyboard, they will be the size of the iPad screen, as you saw after creating the Navigation Controller scene. You then changed the project to be a universal project. This gives you deployment settings for both iPhone and iPad. In those settings you can set which Storyboard is used when running on an iPad and which is used when running on an iPhone. You also have device orientation settings for both iPhone and iPad. iPad apps typically support rotation, so you need to design the iPad interface for the Bands app this way. Running the Bands app on the iPad simulator now uses the new Storyboard and iPad version instead of the iPhone version.

A large majority of the code you wrote for the Bands app on iPhone can also be used on an iPad. The major difference for the Bands app is how some things are presented on an iPad as opposed to an iPhone. Because the code is mostly the same, the best way to add an iPad implementation is to subclass the iPhone code and override only the parts that need to change. By subclassing, the code you have already written will still be executed. This keeps the amount of code in the project to a minimum. It also means that bug fixes will typically need to be made only in the original file.

TRY IT OUT: Subclassing for iPad
 
  1. From the Xcode menu select File ⇒ New ⇒ File.
  2. Select the Cocoa Touch section on the left side of the dialog, select Objective-C class, and click Next.
  3. Name the new class WBABandsListTableViewController_iPad and set the Subclass of selection to the WBABandsListTableViewController class.
  4. Select the WBABandsListTableViewController_iPad.m file from the Project Navigator.
  5. Override the addBandTouched: method using the following code:
    - (IBAction)addBandTouched:(id)sender
    {
        NSLog(@"addBandTouched iPad File");
    }
  6. Select the Main-iPad.storyboard file from the Project Navigator.
  7. Select the Table View and set its Class to the WBABandsListTableViewController_iPad in the Identity Inspector. This Bands List scene is now the same as the Bands List scene in the iPhone Storyboard.
  8. Select the UINavigationItem and set its title to Bands in the Attributes Inspector.
  9. Select the Prototype Cell and set its Style to Basic and its Identifier to Cell.
  10. Drag a UIBarButtonItem to the left side of the UINavigationItem.
  11. Set the Identifier of the UIBarButtonItem to Add; then connect it to the addBandTouched: IBAction.
  12. Run the app in the iPad simulator. You now see the Bands list, as shown in Figure 11-2. Tapping the add button prints to the Xcode debug console.
    阅读 ‧ 电子书库
    FIGURE 11-2
How It Works
When creating new Objective-C classes in Xcode, you can set its parent class. In previous chapters you have used classes created by Apple. In this Try It Out you created a new class that is a subclass of the WBABandsListTableViewController class you created in Chapter 5, “Using Table Views.” By subclassing the new class has all the public properties and methods, including IBOutlets and IBActions, declared in the parent class as well as any protocols you declared in the parent class. You then set the class of the UITableView in the iPad Storyboard to use the iPad class. When the app runs on an iPad, it first looks for methods implemented in the iPad class. Methods that are not overridden use the methods implemented in the parent class instead. For the new iPad UITableView to work with the original class, you needed to set the cell type and reuse identifier. Finally, you added an override for the addBandTouched: method that for now simply writes to the debug console. When running the code in the iPad simulator, the UITableView appears as it does on an iPhone with all the UITableViewDelegate and UITableViewDataSource methods working but with the Add UIBarButtonItem now executing the overridden method instead of the original method.
NOTE When creating the new file you may have noticed the Target for iPad check box. This is for projects that use Mac OS X Interface Builder files, better known as XIB files. Because the Bands app is built using Storyboards instead of XIB files, this check box has no affect. If you were using XIB files and checked the “With XIB for User Interface” box, the XIB would be sized for an iPad.

The next step to creating the universal app is to create the Band Details scene for iPad. The scene will be laid out the same as for iPhone, just a little bigger. Some of the interactions need to be changed for the iPad. You do this in the Learning About Popovers section of this chapter, but to keep the app from crashing while testing, you can override the methods in this Try It Out.

TRY IT OUT: Adding the Band Info View
 
  1. From the Xcode menu select File ⇒ New ⇒ File and create a new subclass of the WBABandDetailsViewController named WBABandDetailsViewController_iPad.
  2. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  3. Override the activityButtonTouched: method using the following code:
    - (IBAction)activityButtonTouched:(id)sender
    {
        NSLog(@"activityButtonTouched iPad File");
    }
  4. Override the deleteButtonTouched: method using the following code:
    - (IBAction)deleteButtonTouched:(id)sender
    {
        NSLog(@"deleteButtonTouched iPad File");
    }
  5. Select the Main-iPad.storyboard from the Project Navigator.
  6. Drag a new View Controller onto the storyboard, and set its Class to the WBABandDetailsViewController_iPad class in Identity Inspector. This is now the Band Details scene, same as in the iPhone Storyboard.
  7. Set the Storyboard ID to bandDetails_iPad in the Identity Inspector.
  8. Re-create the Band Details scene as you did in Chapter 4, “Creating a User Input Form,” without adding the Save and Delete buttons.
  9. Drag a new UIToolbar to the bottom of the view, and add the Save and Delete buttons as UIBarButtonItems separated using a flexible space UIBarButtonItem. The view should look like Figure 11-3 when you finish.
    阅读 ‧ 电子书库
    FIGURE 11-3
  10. Create the segue from the prototype cell in the Bands List scene to the Band Details scene, as you did in Chapter 4.
  11. Set the title in the UINavigationItem to Band in the Band Details scene.
  12. Add the activity UIBarButtonItem to the UINavigationItem.
  13. Connect all the IBOutlets, IBActions, and delegates between the Band Details scene and the WBABandDetailsViewController_iPad.
  14. Select the WBABandsListTableViewController_iPad.m file from the Project Navigator.
  15. Import the WBABandDetailsViewController_iPad.h file using the following code:
    #import "WBABandDetailsViewController_iPad.h"
  16. Modify the addBandTouched: method using the following code:
    - (IBAction)addBandTouched:(id)sender
    {
        NSLog(@"addBandTouched iPad File");
       
        UIStoryboard *iPadStoryBoard =
    [UIStoryboard storyboardWithName:@"Main-iPad" bundle:nil];
        self.bandInfoViewController = (WBABandDetailsViewController_iPad *)
    [iPadStoryBoard instantiateViewControllerWithIdentifier:@"bandInfo_iPad"];
       
        [self presentViewController:self.bandInfoViewController
    animated:YES completion:nil];
    }
  17. Run the app in the iPad simulator. You can now use the Band Details scene, as shown in Figure 11-4.
    阅读 ‧ 电子书库
    FIGURE 11-4
How It Works
First, you created a new subclass of the WBABandDetailsViewController class as you did in the previous Try It Out. In the iPad implementation you added override methods for the activityButtonTouched: and the deleteButtonTouched: methods. Right now these methods write only to the debug console. Next, you created the Band Details scene in the storyboard, set its class to the new WBABandDetailsViewController_iPad class, and set its Storyboard ID. You then recreated the user interface of the Band Details scene using the bigger dimensions of the iPad. The reason you used a UIToolbar instead of standalone UIButtons for Save and Delete will become clear in the Learning About Popovers section of this chapter. Because the WBABandDetailsViewController_iPad class is a subclass of WBABandDetailsViewController, all the IBOutlets, IBActions, and protocol declarations are available in Interface Builder to connect to without needing to add any additional code to the WBABandDetailsViewController_iPad class.
In the WBABandsListTableViewController_iPad implementation, you first imported the WBABandDetailsViewController_iPad.h file. Next you modified the addBandTouched: method to get an instance of UIStoryboard, using the new iPad Storyboard identifier in order to present the correct iPad Band Details scene instead of the iPhone version.

Supporting Rotation Using Auto Layout

If you run the app in the iPad simulator at this point and rotate to landscape, you can see that parts of the user interface of the Band Details scene are out of place. To support rotation you need to add auto layout constraints so that everything adjusts to the new screen size. You first learned about auto layout constraints in Chapter 3, “Starting a New App.” Though auto layout constraints can be complex, you need to know about only three of them to support rotation in the Band Details scene.

The first is the Leading Space to Container constraint. You use this to set a static amount of space between the user interface object and the left edge of the screen. When the device is rotated and the screen size changes, the object continues to be that distance. The second is the Trailing Space to Container constraint that does the same except to the right side of the screen. The other is the Bottom Space to Bottom Layout Guideline. This sets the static space between an object and the bottom of the screen.

TRY IT OUT: Using Auto Layout for Rotation
 
  1. Select the Main-iPad.storyboard from the Project Navigator.
  2. Select the nameTextField, and Control-drag to the left edge of the UIView. When you release the mouse, select a Leading Space to Container auto layout constraint from the dialog.
  3. Add Leading Space to Container auto layout constraints to the notesTextField, touringStatusSegmentedController, and the bottom UIToolbar.
  4. Select the nameTextField, Control-drag to the right edge of the UIView, and add a Trailing Space to Container auto layout constraint.
  5. Add Trailing Space to Container auto layout constraints to the saveNotesButton, notesTextView, ratingsValueLabel, touringStatusSegmentedController, haveSeenLiveSwitch, and the bottom UIToolbar.
  6. Select the bottom UIToolbar, Control-drag to the bottom of the UIView, and add a Bottom Space to Bottom Layout Guide auto layout constraint.
  7. Run the app in the iPad simulator. Rotating to landscape now displays the user interface correctly, as shown in Figure 11-5.
    阅读 ‧ 电子书库
    FIGURE 11-5
How It Works
What you did here was make sure the various user interface objects of the Band Details scene adjust their size when the app is rotated to landscape. The Leading Space to Container layout constraint keeps the same amount of space between the left edge of the screen and the user interface object. The Trailing Space to Container layout constraint does the same but to the right edge of the view. The UIToolbar also requires a Bottom Space to Bottom Layout Guide to keep it anchored to the bottom of the UIView. Now when the iPad is rotated, those user interface objects grow and shrink while keeping those distances the same.

LEARNING ABOUT POPOVERS

The bigger screen of an iPad brings with it new user interaction challenges. iPhone apps, as you have implemented in previous chapters, show scenes that encompass the entire screen. They also show user options that do not display over the entire height of the screen but do stretch the entire width. Because the iPhone screen is smaller, these transitions are comfortable to the user. These types of transitions can be rather jarring on an iPad, leading to a less-than-optimal user experience.

With the release of the iPad, Apple added a new user interface paradigm to the iOS SDK called the UIPopover. A UIPopover is a type of UIView that appears to float over the UIView from which it is displayed. They take up only a small portion of the screen while leaving the rest of the UIView displayed. They also have an arrow pointing back to the part of the UIView the user tapped. This gives users a better user experience because they can concentrate just on the portion of the screen with which they are interacting, yet still keep context of where they are at in the app. To keep apps consistent, Apple’s Human Interface Guidelines require that developers use popovers in iPad apps, so you need to implement them where necessary in the Bands app.

Presenting Action Sheets in Popovers

The first change you need to make is to present the UIActionSheet in a UIPopover. In iPhone apps, UIActionSheets stretch the entire width of the screen and animate from the bottom up. In iPad apps UIActionSheets are shown in a UIPopover with the arrow pointing back to the button or other user interface object the user tapped. To display a UIActionSheet in a UIPopover, you need to modify the code so that the system knows where the arrow should point. The first UIActionSheet you implement is the Delete Band confirmation. By modifying the Band Details scene to use a UIBarButtonItem for the Delete and Save buttons, you can now use the showFromBarButtonItem:animated: method of the UIActionSheet class to present the UIActionSheet in a UIPopover.

One major difference with a UIActionSheet displayed in a UIPopover is that it no longer shows the Cancel button even if you pass in a title for it in the initWithTitle:delegate:cancelButtonTitle:destructiveButtonTitle:otherButtonTitle: method. Instead users can “cancel” by tapping anywhere outside the UIPopover. This is expected behavior.

TRY IT OUT: Presenting Action Sheets in Popovers
 
  1. Select the WBABandDetailsViewController_iPad.h file from the Project Navigator.
  2. Add new IBOutlet for the delete UIBarButtonItem using the following code:
    @property (nonatomic, weak) IBOutlet UIBarButtonItem *deleteBarButtonItem;
  3. Add a property for a UIActionSheet using the following code:
    @property (nonatomic, strong) UIActionSheet *actionSheet;
  4. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  5. Modify the deleteButtonTouched: method using the following code:
    - (void)deleteButtonTouched:(id)sender
    {
        NSLog(@"deleteButtonTouched iPad File");
       
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete Band"
    otherButtonTitles:nil];
        self.actionSheet.tag = WBAActionSheetTagDeleteBand;

        [self.actionSheet showFromBarButtonItem:self.deleteBarButtonItem animated:YES];
    }
  6. Override the actionSheet:clickedButtonAtIndex: method using the following code:
    - (void)actionSheet:(UIActionSheet *)actionSheet
    clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        self.actionSheet = nil;
        [super actionSheet:actionSheet clickedButtonAtIndex:buttonIndex];
    }
  7. Select the Main-iPad.storyboard from the Project Navigator, and connect the Delete button to the deleteBarButtonItem IBOutlet.
  8. Run the app in the iPad simulator. Tapping the Delete button now shows the UIActionSheet in a UIPopover from the Delete UIBarButtonItem, as shown in Figure 11-6.
    阅读 ‧ 电子书库
    FIGURE 11-6
How It Works
The first thing you did was to add an IBOutlet for the delete UIBarButtonItem as well as a new property for a UIActionSheet to the WBABandDetailsViewController_iPad interface. In its implementation you then modified the deleteButtonTouched: method to first look and see if the UIActionSheet property is set. You need this check to prevent showing the delete UIActionSheet multiple times. In the iPhone version of the Bands app the delete UIButton is covered when the UIActionSheet is displayed. This is not the case in the iPad version of the Bands app, so users can continue to tap the delete UIBarButtonItem. This check makes sure your code does not create and show another UIActionSheet should that happen.
The code then creates and presents the UIActionSheet using the showFromBarButtonItem:animated: method. By passing in the deleteBarButtonItem, the UIActionSheet will be displayed in a UIPopover with the arrow pointing back to the deleteBarButtonItem.
You also needed to override the actionSheet:clickedButtonAtIndex: method in order to set the activitySheet property back to nil. Without this override the user would never see another UIActionSheet if they cancel the one shown by tapping outside the UIPopover or if they select an option. Instead of duplicating the code to handle whichever option was selected, you can call the actionSheet:clickedButtonAtIndex: method on super, which will execute the code you have written in the WBABandDetailsViewController parent class.

Using the UIPopoverController

UIActionSheets are not the only user interfaces to be shown in a UIPopover. According to Apple’s Human Interface Guidelines, other things must use a UIPopover when used in an iPad interface. One of those is the UIImagePickerController.

In Chapter 6, “Integrating the Camera and Photo Library in iOS Apps,” you implemented the UIImagePickerController by presenting it over the entire scene. This still works in the iPad implementation, but it could get your app rejected when submitted to Apple. Instead you need to display the UIImagePickerController in a UIPopover by using a UIPopoverController.

The UIPopoverController class can show any subclass of UIViewController. When you present the UIPopover, you need to tell it both where its arrow should point to as well as what direction the arrow should be pointing. You tell the UIPopover what to point to by either displaying it from a UIBarButtonItem or from a CGRect on the screen. A CGRect, as you can recall from Chapter 2, “Introduction to Objective-C,” is a common struct containing a CGPoint and a CGSize (refer to Listing 2-4). It’s a way of denoting a rectangle by its origin point, width, and height. All user interface objects have a frame that is a CGRect, which you can use when displaying a UIPopover.

You tell a UIPopover which direction its arrow should point by using a value of the UIPopoverArrowDirection enumeration. Table 11-1 describes these values. When designing an iPad apps user interface, you may want to specifically tell the system which direction the arrow should point. The Bands app iPad design does not require this, so you can use the UIPopoverArrowDirectionAny constant, as you will see in the following Try It Out.

CONSTANT DESCRIPTION

UIPopoverArrowDirectionUp An arrow that points up with the content shown underneath

UIPopoverArrowDirectionDown An arrow that points down with the content shown above

UIPopoverArrowDirectionLeft An arrow that points left with the content shown on the right

UIPopoverArrowDirectionRight An arrow that points right with the content shown on the left

UIPopoverArrowDirectionAny The system determines which arrow direction should be used based on the frame or button the popover is displayed from.

UIPopoverArrowDirectionUnknown There arrow direction is not known. Used when getting the popoverArrowDirection property of the UIPopoverController when the popover is not presented.

TABLE 11.1: Popover Arrow Constants

TRY IT OUT: Using a Popover Controller
 
  1. Select the WBABandDetailsViewController_iPad.h file from the Project Navigator, and add a new property for a UIPopoverController using the following code:
    @property (nonatomic, strong) UIPopoverController *popover;
  2. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  3. Override the bandImageViewTapDetected method using the following code:
    - (void)bandImageViewTapDetected
    {
        if([UIImagePickerController
    isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
        {
            UIActionSheet *chooseCameraActionSheet = [[UIActionSheet alloc]
    initWithTitle:nil delegate:self cancelButtonTitle:@"Cancel"
    destructiveButtonTitle:nil otherButtonTitles:@"Take with Camera",
    @"Choose from Photo Library", nil];
            chooseCameraActionSheet.tag = WBAActionSheetTagChooseImagePickerSource;
           
            [chooseCameraActionSheet showFromRect:self.bandImageView.frame
    inView:self.view animated:YES];
        }
        else if([UIImagePickerController
    isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
        {
            [self presentPhotoLibraryImagePicker];
        }
        else
        {
            UIAlertView *photoLibraryErrorAlert = [[UIAlertView alloc]
    initWithTitle:@"Error" message:@"There are no photo libraries available"
    delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [photoLibraryErrorAlert show];
        }
    }
  4. Override the bandImageSwipeDetected method using the following code:
    - (void)bandImageViewSwipeDetected
    {
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete Band Image"
    otherButtonTitles:nil];
        self.actionSheet.tag = WBAActionSheetTagDeleteBandImage;
        [self.actionSheet showFromRect:self.bandImageView.frame inView:self.view
    animated:YES];
    }
  5. Override the presentPhotoLibraryImagePicker method using the following code:
    - (void)presentPhotoLibraryImagePicker
    {
        UIImagePickerController *imagePickerController =
    [[UIImagePickerController alloc] init];
        imagePickerController.sourceType =
    UIImagePickerControllerSourceTypePhotoLibrary;
        imagePickerController.delegate = self;
        imagePickerController.allowsEditing = YES;
       
        self.popover = [[UIPopoverController alloc]
    initWithContentViewController:imagePickerController];
        [self.popover presentPopoverFromRect:self.bandImageView.frame inView:self.view
    permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
    }
  6. Override the imagePicker:didFinishPickingMediaWithInfo: method using the following code:
    - (void)imagePickerController:(UIImagePickerController *)picker
    didFinishPickingMediaWithInfo:(NSDictionary *)info
    {
        [super imagePickerController:picker didFinishPickingMediaWithInfo:info];
        [self.popover dismissPopoverAnimated:YES];
        self.popover = nil;
    }
  7. Add the imagePickerControllerDidCancel: method using the following code:
    - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
    {
        [self.popover dismissPopoverAnimated:YES];
        self.popover = nil;
    }
  8. Run the app in the iPad simulator. The image picker is now shown in a popover, as shown in Figure 11-7.
    阅读 ‧ 电子书库
    FIGURE 11-7
How It Works
When tapping the band image on a device with both a camera and a photo library, you need to ask users which one they would like to use. In Chapter 6 you did this using a UIActionSheet. For the iPad version you need to show the UIActionSheet in a UIPopover pointing to the UIImageView. By overriding the bandImageViewTapped method, you can present the UIActionSheet by using the frame property of the UIImageView. The bandImageSwipeDetected method also needs to present the UIActionSheet in a UIPopover, so you override it as well.
Next, you override the presentPhotoLibraryImagePicker to present the UIImagePickerController using a UIPopoverController. You do this by first initializing the UIPopoverController using the initWithContentViewController: method and passing in the UIImagePickerController. You present the UIPopover using the presentPopoverFromRect:inView:permittedArrowDirections:animated: method. For the CGRect you pass in the frame property of the UIImageView. For the view parameter you use the view property of the WBABandDetailsViewController_iPad. Because you don’t care what direction the arrow points, you used the UIPopoverArrowDirectionAny constant. For a better user experience you set the animated property to YES so that the UIPopover animates into view instead of appearing instantly.
After the user picks an image, your code needs to dismiss the UIPopover. You do this by overriding the imagePicker:didFinishPickingMediaWithInfo: method. Because this is an overridden method, you can still execute the code in the parent class by calling the imagePicker:didFinishPickingMediaWithInfo: method on super. After the parent classes code is executed, it returns to the code in the subclass where you dismiss the UIPopover by calling dismissPopoverAnimated:, again passing YES for the animated parameter so that the UIPopover animates out of view instead of disappearing immediately.
In the iPhone version you needed to dismiss the UIImagePickerController if the user taps the Cancel button. In the iPad version you need to dismiss the UIPopover, so you need to override the imagePickerControllerDidCancel: method and call dismissPopoverAnimated: there as well.

Another view controller that should be shown in a UIPopover is the UIActivityViewController. You do this the same way as the UIImagePickerController; by first initializing the UIActivityViewController and then initializing the UIPopoverController using the initWithContentViewController: method.

In the Bands app you have an activity UIBarButton for the Band Details scene. When tapped in the iPhone version, you show a UIActionSheet with the activity options. In the iPad app you need to display the UIActionSheet in a UIPopover pointing to the activity UIBarButtonItem. You do this by using the showFromBarButtonItem:animated: method. If the user selects the share option, you also want the UIActivityViewController in the UIPopoverController to point to the activity UIBarButtonItem. You do this by using the presentPopoverFromBarButtonItem:permittedArrowDirections:animated: method. Again, you can use the UIPopoverArrowDirectionAny constant, though you could use the UIPopoverArrowDirectionUp constant, because that’s the only direction the arrow can point.

UIPopoverController also has a delegate that can tell your code when important things happen with the UIPopover. If users tap outside of the UIPopover, it’s the same as if they had tapped a Cancel button. When this happens, it’s up to your code to actually dismiss the UIPopover. You do this by implementing the popoverControllerDidDismissPopover method of the UIPopoverControllerDelegate.

TRY IT OUT: Showing the UIActivityViewController in a Popover
 
  1. Select the WBABandDetailsViewController_iPad.h file from the Project Navigator.
  2. Declare, the class implements the UIPopoverControllerDelegate using the following code:
    @interface WBABandDetailsViewController_iPad : WBABandDetailsViewController
    <UIPopoverControllerDelegate>
  3. Add a new IBOutlet for the activity UIBarButtonItem using the following code:
    @property (nonatomic, weak) IBOutlet UIBarButtonItem *activityBarButtonItem;
  4. Select the Main-iPad.storyboard from the Project Navigator, and connect the activity UIBarButtonItem to the new IBOutlet.
  5. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  6. Modify the activityButtonTouched: method using the following code:
    - (void)activityButtonTouched:(id)sender
    {
        NSLog(@"activityButtonTouched iPad File");
       
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Share", nil];
        self.actionSheet.tag = WBAActionSheetTagActivity;
        [self.actionSheet showFromBarButtonItem:self.activityBarButtonItem
    animated:YES];
    }
  7. Override the shareBandInfo method using the following code:
    - (void)shareBandInfo
    {
        NSArray *activityItems = [NSArray arrayWithObjects:[self.bandObject
    stringForMessaging], self.bandObject.bandImage, nil];
       
        UIActivityViewController *activityViewController =
    [[UIActivityViewController alloc]initWithActivityItems:activityItems
    applicationActivities:nil];
        [activityViewController setValue:self.bandObject.name forKey:@"subject"];
       
        NSArray *excludedActivityOptions =
    [NSArray arrayWithObjects:UIActivityTypeAssignToContact, nil];
        [activityViewController setExcludedActivityTypes:excludedActivityOptions];
       
        self.popover = [[UIPopoverController alloc]
    initWithContentViewController:activityViewController];
        self.popover.delegate = self;
        [self.popover presentPopoverFromBarButtonItem:self.activityBarButtonItem
    permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
    }
  8. Add the popoverControllerDidDismissPopover method using the following code:
    - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
    {
        [self.popover dismissPopoverAnimated:YES];
        self.popover = nil;
    }
  9. Run the app in the iPad simulator. The UIActivityViewController is now displayed in a popover, as shown in Figure 11-8.
    阅读 ‧ 电子书库
    FIGURE 11-8
How It Works
You first declare that the WBABandDetailsViewController_iPad class implements the UIPopoverControllerDelegate. Next, you added an IBOutlet for the activity UIBarButtonItem so that you can use it when presenting the UIActionSheet as well as the UIActivityViewController popover.
In the implementation you override the activityButtonTouched: method to show the UIActionSheet from the activity UIBarButtonItem. You allow only the Share option at this point, because the other scenes have not been added to the iPad implementation.
You then override the shareBandInfo method. Creating the UIActivityViewController is the same as in the iPhone implementation. You then create and display it using the UIPopoverController displayed from the activity UIBarButtonItem. You also set its delegate to self, so your code is notified when the user taps outside the UIPopover while it is shown. Finally, you implemented the popoverControllerDidDismissPopover: method to dismiss the UIPopover if the user taps anywhere outside the UIPopover.

FINISHING THE IPAD IMPLEMENTATION

You now have all the tools you need to complete the iPad version of the Bands app. The remainder of this chapter will walk you through adding the remaining three scenes to the iPad Storyboard.

The next scene to add to the iPad Storyboard is the Web View scene. This scene shows the Open in Safari option in a UIActionSheet, which will need to be displayed in a UIPopover from the activity UIBarButtonItem. You will use the same approach for this that you implemented in the Band Details scene. The prepareForSegue:sender: method needs to be overridden to use the new iPad class for the Web View scene.

The user interface will almost be identical to the iPhone version except for one difference. In the iPad version the UIWebView needs to be displayed between the UINavigationItem and the UIToolbar. This is because the page will not adjust to display under the UINavigationItem like it does in the iPhone version. You will also learn a new technique for adding auto layout constraints in the following Try It Out.

TRY IT OUT: Adding the Web Search Scene for the iPad
 
  1. From the Xcode menu select File ⇒ New ⇒ File, and create a subclass of the WBAWebViewController called WBAWebViewController_iPad.
  2. Select the WBAWebViewController_iPad.h file from the Project Navigator.
  3. Add an IBOutlet for the action UIBarButtonItem using the following code:
    @property (nonatomic, weak) IBOutlet UIBarButtonItem *actionBarButtonItem;
  4. Add a property for a UIActionSheet using the following code:
    @property (nonatomic, strong) UIActionSheet *actionSheet;
  5. Select the WBAWebViewController_iPad.m file and override the webViewActionButtonTouched: method using the following code:
    - (IBAction)webViewActionButtonTouched:(id)sender
    {
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Open in Safari", nil];
        [self.actionSheet showFromBarButtonItem:self.actionBarButtonItem animated:YES];
    }
  6. Override the actionSheet:clickedButtonAtIndex: method with the following code:
    - (void)actionSheet:(UIActionSheet *)actionSheet
    clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        self.actionSheet = nil;
        [super actionSheet:actionSheet clickedButtonAtIndex:buttonIndex];
    }
  7. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  8. Import the WBAWebViewController_iPad.h file using the following code:
    #import "WBAWebViewController_iPad.h"
  9. Add the prepareForSegue:sender: method using the following code:
    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {
        if([segue.destinationViewController class] == [WBAWebViewController_iPad class])
        {
            WBAWebViewController_iPad *webViewController =
    segue.destinationViewController;
            webViewController.bandName = self.bandObject.name;
        }
    }
  10. Modify the activityButtonTouched: method using the following code:
    - (void)activityButtonTouched:(id)sender
    {
        NSLog(@"activityButtonTouched iPad File");
       
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Share", @"Search the Web", nil];
        self.actionSheet.tag = WBAActionSheetTagActivity;
        [self.actionSheet showFromBarButtonItem:self.activityBarButtonItem
    animated:YES];
    }
  11. Select the Main-iPad.storyboard from the Project Navigator.
  12. Drag a new View Controller onto the Storyboard, and set its class to the WBAWebViewController_iPad class in the Identity Inspector. This is the iPad version of the Web View scene.
  13. Create a push segue named webViewSegue from the Band Details scene to the Web View scene.
  14. Create the Web View scene user interface as you did in Chapter 8, “Using Web Views,” by adding the UIWebView along with the UIToolbar and UIBarButtonItems for navigation.
  15. Adjust the UIWebView so that it sits between the UINavigationItem and the UIToolbar.
  16. Select the UIWebView and then click the Pin auto layout button. In the dialog, select the lines at the top of the dialog for all four sides of the UIWebView. This will change the lines from dashed to solid as shown in Figure 11-9.
    阅读 ‧ 电子书库
    FIGURE 11-9
  17. Select the UIToolbar, click the Pin auto layout button, select the lines for the left, right, and bottom. Then, click the Add 3 Constraints button.
  18. Connect the IBOutlets, IBActions, and delegate to the WBAWebViewController_iPad.
  19. Run the app in the iPad simulator. The Search the Web feature now works, as shown in Figure 11-10.
    阅读 ‧ 电子书库
    FIGURE 11-10
How It Works
The first step in creating the iPad version of the Web Search scene is creating a new WBAWebViewController_iPad class as a subclass of the WBAWebViewController you implemented in Chapter 8, “Using Web Views.” The iPad implementation needs to show the UIActionSheet in a UIPopover. You implemented this in the iPad Web View scene the same as the activity options in the Band Details scene. In the WBAWebViewController_iPad you added an IBOutlet for the activity UIBarButtonItem as well as a property for the UIActionSheet. You then override the webViewActionButtonTouched: method to perform the same check on the UIActionSheet property to prevent multiple UIPopovers before creating and displaying the UIActionSheet in a UIPopover from the activity UIBarButtonItem. You also added an override of the actionSheet:clickedButtonAtIndex: method to set the UIActionSheet property back to nil so that it can be displayed again. In the WBABandDetailsViewController_iPad implementation you added an override for the prepareForSegue:sender: method to check for the WBAWebSearchViewController_iPad class. Without this override the iPhone implementation that checks for the WBAWebSearchViewController class to set the bandName would be called. This would fail, because it’s a different class in the iPad implementation, so the bandName would not be set.
You re-created the user interface for the Web View scene the same as it appears in the iPhone version. To support rotation you added the auto layout constraints using the Pin auto layout constraint dialog. Instead of needing to Control-drag to the various sides of the UIView, you can use this dialog to quickly add all the constraints you need by simply clicking the sides you want the constraints to be added to.

The next scene to add is the Map Search scene. This scene needs the same implementation of the UIActionSheet in a UIPopover as well as auto layout constraints to support rotation. The following Try It Out is close to identical to the previous Try It Out.

TRY IT OUT: Adding the Find Local Record Store Feature for iPad
 
  1. From the Xcode menu select File ⇒ New ⇒ File, and create a new subclass of the WBAMapViewController called WBAMapViewController_iPad.
  2. Select the WBAMapViewController_iPad.h file from the Project Navigator.
  3. Add an IBOutlet for the action bar button item using the following code:
    @property (nonatomic, weak) IBOutlet UIBarButtonItem *actionBarButtonItem;
  4. Add a property for an action sheet using the following code:
    @property (nonatomic, strong) UIActionSheet *actionSheet;
  5. Select the WBAMapViewController_iPad.m file from the Project Navigator, and override the actionButtonTouched: method using the following code:
    - (IBAction)actionButtonTouched:(id)sender
    {
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Map View", @"Satellite View", @"Hybrid View", nil];
        [self.actionSheet showFromBarButtonItem:self.actionBarButtonItem animated:YES];
    }
  6. Override the actionSheet:clickedButtonAtIndex: method with the following code:
    - (void)actionSheet:(UIActionSheet *)actionSheet
    clickedButtonAtIndex:(NSInteger)buttonIndex
    {
        self.actionSheet = nil;
        [super actionSheet:actionSheet clickedButtonAtIndex:buttonIndex];
    }
  7. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator, and modify the activityButtonTouched: method using the following code:
    - (void)activityButtonTouched:(id)sender
    {
        NSLog(@"activityButtonTouched iPad File");
       
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Share", @"Search the Web",
    @"Find Local Record Stores", nil];
        self.actionSheet.tag = WBAActionSheetTagActivity;
        [self.actionSheet showFromBarButtonItem:self.activityBarButtonItem
    animated:YES];
    }
  8. Select the Main-iPad.storyboard from the Project Navigator.
  9. Drag a new View Controller onto the storyboard, and set its class to the WBAMapViewController_iPad class in the Identity Inspector. This is now the iPad version of the Map Search scene.
  10. Create a push segue named mapViewSegue from the Band Details scene to the Map View scene.
  11. Create a push segue named recordStoreWebSearchSegue from the Map View scene to the Web View scene.
  12. Create the Map View scene user interface as you did in Chapter 9, “Exploring Maps and Local Search.”
  13. Connect the IBOutlets, IBActions, and delegates to the WBAMapViewController_iPad class.
  14. Select the MKMapView, click the Pin auto layout button, select the lines for all four sides of the MKMapView, then click the Add 4 Constraints button.
  15. Run the app in the iPad simulator. The Find Local Record Stores feature now works, as shown in Figure 11-11.
    阅读 ‧ 电子书库
    FIGURE 11-11
How It Works
This Try It Out follows the same pattern you implemented in both the Band Details scene and the Web View scene. The UIActionSheet to change the map type needs to be shown in a UIPopover from the activity UIBarButtonItem. You added properties for the UIActionSheet and UIBarButtonItem to achieve this. In the actionButtonTouched: implementation you again check if a UIActionSheet is already being displayed before creating and displaying a new one from the UIBarButtonItem. You also override the actionSheet:clickedButtonAtIndex: to set the UIActionSheet back to nil.
The only user interface object that needs auto layout constraints to support rotation is the MKMapView. You added the four constraints using the Pin auto layout constraint dialog.

The last feature you need to add to the iPad implementation is the Search iTunes for Tracks feature. In the iPhone implementation you show a UIActionSheet with the Preview and Open in iTunes options. In the iPad implementation you need to show this in a UIPopover that points to the selected row in the UITableView. You do this by overriding the tableView:didSelectRowAtIndexPath: method and then using the rectForRowAtIndexPath: method of the UITableView class to get the CGRect of the selected row that the UIPopover should point to. Because the user cannot tap the UITableView multiple times and continue to have the UIActionSheet displayed, you do not need to check if one is visible before displaying it. This means you do not need a property for the UIActionSheet as you have implemented in the other scenes. The WBABandDetailsViewController_iPad needs its prepareForSegue:sender: method updated again in order to set the bandName, same as you implemented for the iPad version of the Web View scene.

For the user interface, the UITableView already has the auto layout constraints it needs to support rotation, so you do not need to add any in the following Try It Out.

TRY IT OUT: Adding the iTunes Search Feature for an iPad
 
  1. From the Xcode menu select File ⇒ New ⇒ File, and create a new subclass of the WBAiTunesSearchViewController called WBAiTunesSearchViewController_iPad.
  2. Select the WBAiTunesSearchViewController_iPad.m file from the Project Navigator.
  3. Override the tableView:didSelectRowAtIndexPath: method using the following code:
    - (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
    delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Preview Track", @"Open in iTunes", nil];
        CGRect selectedRowRect = [self.tableView rectForRowAtIndexPath:indexPath];
        [actionSheet showFromRect:selectedRowRect inView:self.view animated:YES];
    }
  4. Select the Main-iPad.storyboard from the Project Navigator.
  5. Drag a new Table View Controller onto the Storyboard, and set its class to the WBAiTunesSearchViewController_iPad class in the Identity Inspector. This is now the iPad version of the iTunes Search scene.
  6. Create a push segue named iTunesSearchSegue from the Band Details scene to the iTunes Search scene.
  7. Select the prototype cell; then set its style to Subtitle and its reuse identifier to trackCell.
  8. Add a UISearchBar to the top of the UITableView as you did in the previous chapter.
  9. Connect the IBOutlet and delegate of the search bar to the WBAiTunesSearchViewController_iPad.
  10. Select the WBABandDetailsViewController_iPad.m file from the Project Navigator.
  11. Import the WBAiTunesSearchViewController_iPad.h file using the following code:
    #import "WBAiTunesSearchViewController_iPad.h"
  12. Modify the prepareForSegue:sender: method using the following code:
    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {
        if([segue.destinationViewController class] == [WebViewController_iPad class])
        {
            WebViewController_iPad *webViewController =
    segue.destinationViewController;
            webViewController.bandName = self.bandObject.name;
        }
       
    else if ([segue.destinationViewController class] ==
    [WBAiTunesSearchViewController_iPad class])
        {
            WBAiTunesSearchViewController_iPad *iTunesSearchViewController =
    segue.destinationViewController;
            iTunesSearchViewController.bandName = self.bandObject.name;
        }

    }
  13. Modify the activityButtonTouched: method using the following code:
    - (void)activityButtonTouched:(id)sender
    {
        NSLog(@"activityButtonTouched iPad File");
       
        if(self.actionSheet)
            return;
       
        self.actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self
    cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil
    otherButtonTitles:@"Share", @"Search the Web", @"Find Local Record Stores",
    @"Search iTunes for Tracks", nil];
        self.actionSheet.tag = WBAActionSheetTagActivity;
        [self.actionSheet showFromBarButtonItem:self.activityBarButtonItem
    animated:YES];
    }
  14. Run the app in the iPad simulator. The iTunes Track Search feature is now available, as shown in Figure 11-12.
    阅读 ‧ 电子书库
    FIGURE 11-12
How It Works
For the iPad implementation you override tableView:didSelectRowAtIndexPath: to call the rectForRowAtIndexPath: method of the UITableView to get the CGRect of the selected row. You then use the CGRect of the row to present the UIActionSheet in a UIPopover using the showFromRect:inView: method of the UIActionSheet.
Next, you created the iPad scene and re-created the segues and cell identifiers so that the iPhone code continues to work. You then modified the prepareForSegue:sender: method to look for the WBAiTunesSearchViewController_iPad class to set the bandName. Finally you added the option back to the Band Details activity options.

SUMMARY

Creating a universal app can increase your user base by giving iPad users an app designed for their device. Your project and code do not need to change dramatically to do this. By adding a new Storyboard designed for the iPad and subclassing your iPhone implementation code, you can add support for the iPad and its design patterns quickly and efficiently.

EXERCISES
 
  1. What is the name of the auto layout constraint that keeps a static amount of space between a user interface object and the right edge of the screen?
  2. What method of the UIActionSheet class presents the action sheet in a UIPopover pointing to a UIBarButtonItem in a toolbar?
  3. How can your code know when a user taps outside of a UIPopover presented from a UIPopoverController?
WHAT YOU LEARNED IN THIS CHAPTER
TOPIC KEY CONCEPTS
Universal Apps Apps can be written as a single project designed to appear differently, depending on if it’s running on an iPhone or an iPad.
Popovers The iOS SDK includes a user interface paradigm for iPads that displays a user prompt or another view hovering over the main content of the app.
Auto Layout When an iOS device is rotated, the screen size changes. The iOS SDK includes the concept of auto layout constraints that set the rules for how the user interface should adjust when the user rotates their device.