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 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:
From inside a navigation button, an instance of
UIBarButtonItem
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.
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.
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.