1.29. Displaying Popovers with UIPopoverController

Problem

You want to display content on an iPad without blocking the whole screen.

Solution

Use popovers.

Discussion

Popovers are used to display additional information on the iPad screen. An example can be seen in the Safari app on the iPad. When the user taps on the Bookmarks button, she will see a popover displaying the bookmarks content on the screen (see Figure 1-78).

The bookmarks popover in the Safari app on an iPad

Figure 1-78. The bookmarks popover in the Safari app on an iPad

The default behavior of popovers is that when the user taps somewhere outside the region of the popover, the popover will automatically get dismissed. You can ask the popover to not get dismissed if the user taps on specific parts of the screen, as we will see later. Popovers present their content by using a view controller. Note that you can also present navigation controllers inside popovers, because navigation controllers are a subclass of UIViewController.

Warning

Popovers can be used only on iPad devices. If you have a view controller whose code runs on both an iPad and on an iPhone, you need to make sure that you are not instantiating the popover on a device other than the iPad.

Popovers can be presented to the user in two ways:

  1. From inside a navigation button, an instance of UIBarButtonItem

  2. From inside a rectangular area in a view

When a device orientation is changed (the device is rotated), popovers are either dismissed or hidden temporarily. You need to make sure that you give your users a good experience by redisplaying the popover after the orientation change has settled, if possible. In certain cases, your popover might get dismissed automatically after an orientation change. For instance, if the user taps on a navigation button in landscape mode you might display a popover on the screen. Suppose your app is designed so that when the orientation changes to portrait, the navigation button is removed from the navigation bar for some reason. Now, the correct user experience would be to hide the popover associated with that navigation bar after the orientation of the device is changed to portrait. In some instances, though, you will need to play with popovers a bit to give your users a good experience, because handling device orientation is not always as straightforward as in the aforementioned scenario.

To create the demo popover app, we need to first come up with a strategy based on our requirements. For this example, we want to build an app with a view controller loaded inside a navigation controller. The root view controller will display a + button on the right corner of its navigation bar. When the + button is tapped on an iPad device, it will display a popover with two buttons on it. The first button will say “Photo,” and the second button will say “Audio.” When the same navigation button is tapped on an iPhone device, we will display an alert view with three buttons: the two aforementioned buttons, and a cancel button so that the user can cancel the alert view if he wishes to. When these buttons are tapped (whether on the alert view on an iPhone or the popover on an iPad), we won’t really do anything. We will simply dismiss the alert view or the popover.

Go ahead and create a Single View universal project in Xcode and name the project “Displaying Popovers with UIPopoverController.” Then, using the technique shown in Recipe 6.1, add a navigation controller to your storyboard so that your view controllers will have a navigation bar.

After this, we need to go into the definition of our root view controller and define a property of type UIPopoverController:

#import "ViewController.h"

@interface ViewController () <UIAlertViewDelegate>
@property (nonatomic, strong) UIPopoverController *myPopoverController;
@property (nonatomic, strong) UIBarButtonItem *barButtonAdd;
@end

@implementation ViewController

<# Rest of your code goes here #>

You can see that we are also defining a property called barButtonAdd in our view controller. This is the navigation button that we will add on our navigation bar. Our plan is to display our popover when the user taps on this button (you can read more about navigation buttons in Recipe 1.15). However, we need to make sure we instantiate the popover only if the device is an iPad. Before we implement our root view controller with the navigation button, let’s go ahead and create a subclass of UIViewController and name it PopoverContentViewController. We will display the contents of this view controller inside our popover later. See Recipe 1.9 for information about view controllers and ways of creating them.

The content view controller displayed inside the popover will have two buttons (as per our requirements). However, this view controller will need to have a reference to the popover controller in order to dismiss the popover when the user taps on any of the buttons. For this, we need to define a property in our content view controller to refer to the popover:

#import <UIKit/UIKit.h>

@interface PopoverContentViewController : UIViewController

/* We shouldn't define this as strong. That will create a retain cycle
 between the popover controller and the content view controller since the
 popover controller retains the content view controller and the view controller 
 will retain the popover controller */
@property (nonatomic, weak) UIPopoverController *myPopoverController;

@end

And, also inside the implementation file of our content view controller, we declare our bar buttons:

#import "PopoverContentViewController.h"

@interface PopoverContentViewController ()
@property (nonatomic, strong) UIButton *buttonPhoto;
@property (nonatomic, strong) UIButton *buttonAudio;
@end

@implementation PopoverContentViewController

<# Rest of your code goes here #>

After this, we’ll create our two buttons in the content view controller and link them to their action methods. These methods will take care of dismissing the popover that is displaying this view controller. Remember, the popover controller will be responsible for assigning itself to the popoverController property of the content view controller:

- (BOOL) isInPopover{

    Class popoverClass = NSClassFromString(@"UIPopoverController");

    if (popoverClass != nil &&
        UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad &&
        self.myPopoverController != nil){
        return YES;
    } else {
        return NO;
    }

}

- (void) gotoAppleWebsite:(id)paramSender{

    if ([self isInPopover]){
        /* Go to website and then dismiss popover */
        [self.myPopoverController dismissPopoverAnimated:YES];
    } else {
        /* Handle case for iPhone */
    }

}

- (void) gotoAppleStoreWebsite:(id)paramSender{

    if ([self isInPopover]){
        /* Go to website and then dismiss popover */
        [self.myPopoverController dismissPopoverAnimated:YES];
    } else {
        /* Handle case for iPhone */
    }

}

- (void)viewDidLoad{
    [super viewDidLoad];

    self.preferredContentSize = CGSizeMake(200.0f, 125.0f);

    CGRect buttonRect = CGRectMake(20.0f,
                                   20.0f,
                                   160.0f,
                                   37.0f);

    self.buttonPhoto = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.buttonPhoto setTitle:@"Photo"
                      forState:UIControlStateNormal];
    [self.buttonPhoto addTarget:self
                         action:@selector(gotoAppleWebsite:)
               forControlEvents:UIControlEventTouchUpInside];

    self.buttonPhoto.frame = buttonRect;

    [self.view addSubview:self.buttonPhoto];


    buttonRect.origin.y += 50.0f;
    self.buttonAudio = [UIButton buttonWithType:UIButtonTypeSystem];

    [self.buttonAudio setTitle:@"Audio"
                      forState:UIControlStateNormal];
    [self.buttonAudio addTarget:self
                         action:@selector(gotoAppleStoreWebsite:)
               forControlEvents:UIControlEventTouchUpInside];

    self.buttonAudio.frame = buttonRect;

    [self.view addSubview:self.buttonAudio];

}

Now in the viewDidLoad method of our root view controller, we will create our navigation button. Based on the device type, when the navigation bar is tapped, we will display either a popover (on the iPad) or an alert view (on the iPhone):

- (void)viewDidLoad{
    [super viewDidLoad];

    /* See if this class exists on the iOS running the app */
    Class popoverClass = NSClassFromString(@"UIPopoverController");

    if (popoverClass != nil &&
        UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){

        PopoverContentViewController *content =
        [[PopoverContentViewController alloc] initWithNibName:nil
                                                       bundle:nil];

        self.myPopoverController = [[UIPopoverController alloc]
                                  initWithContentViewController:content];

        content.myPopoverController = self.myPopoverController;

        self.barButtonAdd =
        [[UIBarButtonItem alloc]
         initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
         target:self
         action:@selector(performAddWithPopover:)];

    } else {

        self.barButtonAdd =
        [[UIBarButtonItem alloc]
         initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
         target:self
         action:@selector(performAddWithAlertView:)];

    }

    [self.navigationItem setRightBarButtonItem:self.barButtonAdd
                                      animated:NO];

}

Note

The popover controller sets a reference to itself in the content view controller after its initialization. This is very important. A popover controller cannot be initialized without a content view controller. Once the popover is initialized with a content view controller, you can go ahead and change the content view controller in the popover controller, but not during the initialization.

We have elected the performAddWithPopover: method to be invoked when the + navigation bar button is tapped on an iPad device. If the device isn’t an iPad, we’ve asked the + navigation bar button to invoke the performAddWithAlertView: method. Let’s go ahead and implement these methods and also take care of the delegate methods of our alert view, so that we know what alert view button the user tapped on an iPhone:

- (NSString *) photoButtonTitle{
    return @"Photo";
}

- (NSString *) audioButtonTitle{
    return @"Audio";
}

- (void)          alertView:(UIAlertView *)alertView
  didDismissWithButtonIndex:(NSInteger)buttonIndex{

    NSString *buttonTitle = [alertView buttonTitleAtIndex:buttonIndex];

    if ([buttonTitle isEqualToString:[self photoButtonTitle]]){
        /* Adding a photo ... */
    }
    else if ([buttonTitle isEqualToString:[self audioButtonTitle]]){
        /* Adding an audio... */
    }

}

- (void) performAddWithAlertView:(id)paramSender{

    [[[UIAlertView alloc] initWithTitle:nil
                                message:@"Add..."
                               delegate:self
                      cancelButtonTitle:@"Cancel"
                      otherButtonTitles:
      [self photoButtonTitle],
      [self audioButtonTitle], nil] show];

}

- (void) performAddWithPopover:(id)paramSender{

    [self.myPopoverController
     presentPopoverFromBarButtonItem:self.barButtonAdd
     permittedArrowDirections:UIPopoverArrowDirectionAny
     animated:YES];

}

If you now run your app on iPad Simulator and tap the + button on the navigation bar, you will see an interface similar to Figure 1-79.

Our simple popover displayed when a navigation button was tapped

Figure 1-79. Our simple popover displayed when a navigation button was tapped

If you run the same universal app on iPhone Simulator and tap the + button on the navigation bar, you will see results similar to Figure 1-80.

Popovers are replaced by alert view in a universal app

Figure 1-80. Popovers are replaced by alert view in a universal app

We used an important property of our content view controller: preferredContentSize. The popover, when displaying its content view controller, will read the value of this property automatically and will adjust its width and height to this size. Also, we used the presentPopoverFromBarButtonItem:permittedArrowDirections:animated: method of our popover in our root view controller to display the popover over a navigation bar button. The first parameter to this method is the navigation bar button from which the popover controller has to be displayed. The second parameter specifies the direction of the popover when it appears, in relation to the object from which it appears. For example, in Figure 1-79, you can see that our popover’s arrow is pointing up toward the navigation bar button. The value that you pass to this parameter must be of type UIPopoverArrowDirection:

typedef NS_OPTIONS(NSUInteger, UIPopoverArrowDirection) {
    UIPopoverArrowDirectionUp = 1UL << 0,
    UIPopoverArrowDirectionDown = 1UL << 1,
    UIPopoverArrowDirectionLeft = 1UL << 2,
    UIPopoverArrowDirectionRight = 1UL << 3,
    UIPopoverArrowDirectionAny = UIPopoverArrowDirectionUp |
    UIPopoverArrowDirectionDown |
    UIPopoverArrowDirectionLeft |
    UIPopoverArrowDirectionRight,
    UIPopoverArrowDirectionUnknown = NSUIntegerMax
};

Get iOS 7 Programming Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.