Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (12.71 MB, 929 trang )
Figure 9-1. Two popovers
• Both views continue to appear side by side; the second view is narrower than it is
in landscape orientation, because the screen is narrower. Apple’s Settings app is an
example.
• Only the second view appears, with an option to summon the first view by tapping
a bar button item (or, optionally, by swiping to the right). When the first view is
summoned in this way, it is a popover, although, since iOS 5.1, it hasn’t looked like
one — it has no arrow, its height is the full height of the screen, and it appears and
disappears by sliding. Apple’s Mail app is an example (Figure 9-2).
Like popovers, a split view may be regarded as an evolutionary link between the smaller
iPhone interface and the larger iPad interface. On the iPhone, you might have a master–
detail architecture in a navigation interface, where the master view is a table view, and
the detail view is a completely different view pushed onto the navigation stack in place
of the master view (Chapter 8). On the iPad, the large screen can accommodate the
master view and the detail view simultaneously; the split view is a built-in way to do
that. It is no coincidence that the Master–Detail Application template in Xcode generates
a navigation interface for the iPhone and a split view for the iPad.
474
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info
Figure 9-2. A familiar split view interface
Before iOS 5, UISplitViewController was the only legal way in which a single view
controller could display the views of two child view controllers side by side. Nowadays,
you are free to design your own custom parent view controllers (Chapter 6), so UISplit‐
ViewController is of diminished value. Nevertheless, it’s built-in and easy to use.
Preparing a Popover
Before you can display a popover, you’ll need a UIPopoverController, along with a view
controller (UIViewController) whose view the popover will contain. UIPopover‐
Controller is not a UIViewController subclass! The view controller is the UIPopover‐
Controller’s contentViewController. You’ll set this property initially through
UIPopoverController’s designated initializer, initWithContentViewController:. Sub‐
sequently, if you like, you can swap out a popover controller’s view controller (and hence
its contained view) by calling setContentViewController:animated:.
Here’s how the UIPopoverController for the first popover in Figure 9-1 gets its content.
I have a UIViewController subclass, NewGameController. NewGameController’s view
contains a grouped table (whose code I showed you in Chapter 8) and a UIPickerView
(see Chapter 12), and is itself the data source and delegate for both. I instantiate New‐
GameController and use this instance as the root view controller of a UINavigation‐
Controller, giving its navigationItem a Done leftBarButtonItem and a Cancel rightBarButtonItem. (I don’t really intend to do any navigation, but the navigation control‐
ler’s navigation bar is a convenient way of adding the two buttons to the interface.) The
UINavigationController then becomes a UIPopoverController’s view controller:
Preparing a Popover
www.it-ebooks.info
|
475
NewGameController* dlg = [NewGameController new];
UIBarButtonItem* b = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem: UIBarButtonSystemItemCancel
target: self
action: @selector(cancelNewGame:)];
dlg.navigationItem.rightBarButtonItem = b;
b = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem: UIBarButtonSystemItemDone
target: self
action: @selector(saveNewGame:)];
dlg.navigationItem.leftBarButtonItem = b;
UINavigationController* nav =
[[UINavigationController alloc] initWithRootViewController:dlg];
UIPopoverController* pop =
[[UIPopoverController alloc] initWithContentViewController:nav];
That code doesn’t cause the popover to appear on the screen! I’ll come to that in the
next section. Observe also that I have done nothing as yet about retaining the
UIPopoverController; if I fail to do that, it will vanish in a puff of smoke. I’ll talk about
that in the next section as well.
Popover Size
The popover controller also needs to know the size of the view it is to display, which
will determine the size of the popover. You can provide the popover size in one of two
ways:
UIPopoverController’s popoverContentSize property
This property can be set before the popover appears; it can also be changed while
the popover is showing, with setPopoverContentSize:animated:.
UIViewController’s preferredContentSize property
The UIViewController in question is the UIPopoverController’s contentViewController (or is contained by that view controller, as in a tab bar interface or
navigation interface). This approach often makes more sense, because a UIView‐
Controller will generally know its own view’s ideal size. If a view controller is to be
instantiated from a nib, this value can be set in the Attributes inspector.
(This property is new in iOS 7, and replaces the older contentSizeForViewInPopover property, which is now deprecated.)
The default popover size is {320,480} (the size of an original iPhone screen); the doc‐
umentation suggests that a maximum width of 600 is permitted — the second popover
in Figure 9-1 adopts this width — but in fact there doesn’t seem to be any penalty for
using a larger width. The popover’s size is automatically restricted, however, based on
the amount of screen space actually available, and your view may need to be prepared
(through autoresizing or constraints) for the possibility that it won’t be shown at the
size you requested.
476
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info
In the case of the first popover in Figure 9-1, the NewGameController sets its own
preferredContentSize in viewDidLoad, based on the size of its main view as it comes
from the nib:
self.preferredContentSize = self.view.bounds.size;
The popover itself, however, will need to be somewhat taller, because the NewGame‐
Controller is embedded in a UINavigationController, whose navigation bar occupies
additional vertical space. Delightfully, the UINavigationController takes care of that
automatically; its own preferredContentSize adds the necessary height.
If the UIPopoverController and the UIViewController have different settings for their
respective content size properties at the time the popover is initially displayed, the
UIPopoverController’s setting wins. But once the popover is visible, if either property
is changed (not merely set to the value it already has), the change is obeyed; if the
UIViewController’s preferredContentSize is changed, the UIPopoverController
adopts that value as its popoverContentSize and the popover’s size is adjusted accord‐
ingly (with animation).
If a popover’s contentViewController is a UINavigationController, and a view con‐
troller is pushed onto or popped off of its stack, then if the new current view controller’s
preferredContentSize differs from that of the previously displayed view controller,
my experiments suggest that the popover’s width will change to match the new width,
but the popover’s height will change only if the new height is taller. This feels like a bug.
A possible workaround is to implement the UINavigationController’s delegate method
navigationController:didShowViewController:animated:, so as to set the naviga‐
tion controller’s preferredContentSize explicitly:
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated {
navigationController.preferredContentSize =
viewController.preferredContentSize;
}
The result is still cosmetically disturbing, however, and it is probably better not to do
that sort of thing in the first place.
Popover Appearance Customization
By default, a popover controller in iOS 7 takes charge of the background color of its
view controller’s view, including the navigation bar in a navigation interface. You can
see this in Figure 9-1; the first popover, along with its navigation bar, has automatically
adopted a somewhat transparent cream color that is apparently derived somehow from
the color of what’s behind it.
Preparing a Popover
www.it-ebooks.info
|
477
Figure 9-3. A very silly popover
If you don’t want this automatic background color and transparency, you can set the
popover controller’s backgroundColor (new in iOS 7). You can change the navigation
bar’s color separately, and customize the position and appearance of the navigation bar’s
bar button items; see Chapter 12.
You can also customize the outside of the popover — that is, the “frame” surrounding
the content, along with the arrow that points to the button that summoned it. To do so,
you set the UIPopoverController’s popoverBackgroundViewClass to your own subclass
of UIPopoverBackgroundView (a UIView subclass) — at which point you can achieve
just about anything you want, including the very silly popover shown in Figure 9-3.
Configuring your UIPopoverBackgroundView subclass is a bit tricky, because this single
view is responsible for drawing both the arrow and the frame. Thus, in a complete and
correct implementation, you’ll have to draw differently depending on the arrow direc‐
tion, which you can learn from the UIPopoverBackgroundView’s arrowDirection
property; it will be one of the following:
• UIPopoverArrowDirectionUp, UIPopoverArrowDirectionDown
• UIPopoverArrowDirectionLeft, UIPopoverArrowDirectionRight
I’ll give a simplified example in which I cheat by assuming that (as in Figure 9-3) the
arrow direction will be UIPopoverArrowDirectionUp. Drawing the frame is easy: here,
I divide the view’s overall rect into two areas, the arrow area on top (its height is a
478
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info
#defined constant, ARHEIGHT) and the frame area on the bottom, and I draw the frame
into the bottom area as a resizable image (Chapter 2):
UIImage* linOrig = [UIImage imageNamed: @"linen.png"];
CGFloat capw = linOrig.size.width / 2.0 - 1;
CGFloat caph = linOrig.size.height / 2.0 - 1;
UIImage* lin = [linOrig
resizableImageWithCapInsets:UIEdgeInsetsMake(caph, capw, caph, capw)
resizingMode:UIImageResizingModeTile];
// ... draw arrow here ...
CGRect arrow;
CGRect body;
CGRectDivide(rect, &arrow, &body, ARHEIGHT, CGRectMinYEdge);
[lin drawInRect:body];
I omitted the drawing of the arrow; now let’s insert it. The UIPopoverBackgroundView
has arrowHeight and arrowBase class methods that you’ve overridden to describe the
arrow dimensions to the runtime. (In my code, their values are provided by two
#defined constants, ARHEIGHT and ARBASE; I’ve set them both to 20.) My arrow will
consist simply of a texture-filled isosceles triangle, with an excess base consisting of a
rectangle joining it to the frame. The UIPopoverBackgroundView also has an arrowOffset property that the runtime has set to tell you where to draw the arrow: this offset
measures the positive distance between the center of the view’s edge and the center of
the arrow. However, the runtime will have no hesitation in setting the arrowOffset all
the way at the edge of the view, or even beyond its bounds (in which case it won’t be
drawn); to prevent this, I provide a maximum offset limit:
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextSaveGState(con);
CGFloat proposedX = self.arrowOffset;
CGFloat limit = 22.0;
CGFloat maxX = rect.size.width/2.0 - limit;
if (proposedX > maxX)
proposedX = maxX;
if (proposedX < limit)
proposedX = limit;
CGContextTranslateCTM(con, rect.size.width/2.0 + proposedX - ARBASE/2.0, 0);
CGContextMoveToPoint(con, 0, ARHEIGHT);
CGContextAddLineToPoint(con, ARBASE / 2.0, 0);
CGContextAddLineToPoint(con, ARBASE, ARHEIGHT);
CGContextClosePath(con);
CGContextAddRect(con, CGRectMake(0,ARHEIGHT,ARBASE,15));
CGContextClip(con);
[lin drawAtPoint:CGPointMake(-40,-40)];
CGContextRestoreGState(con);
The thickness of the four sides of the frame is dictated by implementing the contentViewInsets class method.
Preparing a Popover
www.it-ebooks.info
|
479
Summoning and Dismissing a Popover
A popover is made to appear onscreen by sending a UIPopoverController one of the
following messages (and the UIPopoverController’s popoverVisible property then be‐
comes YES):
• presentPopoverFromRect:inView:permittedArrowDirections:animated:
• presentPopoverFromBarButtonItem:permittedArrowDirections:animated:
The difference between the two methods lies in how you specify the region to which
the popover’s arrow will point. With the first method, you can provide any CGRect with
respect to any visible UIView’s coordinate system; for example, to make the popover
emanate from a UIButton, you could provide the UIButton’s frame with respect to its
superview, or (better) the UIButton’s bounds with respect to itself. But you can’t do that
with a UIBarButtonItem, because a UIBarButtonItem isn’t a UIView and doesn’t have
a frame or bounds; hence the second method is provided.
The permitted arrow directions restrict which sides of the popover the arrow can appear
on. It’s a bitmask, and your choices are:
• UIPopoverArrowDirectionUp, UIPopoverArrowDirectionDown
• UIPopoverArrowDirectionLeft, UIPopoverArrowDirectionRight
• UIPopoverArrowDirectionAny
Usually, you’d specify UIPopoverArrowDirectionAny, allowing the runtime to put the
arrow on whatever side it feels is appropriate.
Even if you specify a particular arrow direction, you still have no precise control over a
popover’s location. However, you do get some veto power: set the UIPopoverController’s
popoverLayoutMargins beforehand to a UIEdgeInsets stating the margins, with respect
to the root view bounds, within which the popover must appear. If the inset that you
give is so large that the arrow can no longer touch the presenting rect, it may be ignored,
or the arrow may become disconnected from its presenting rect; you probably shouldn’t
do that.
Popover Segues
If you’re using a storyboard, you can draw (Control-drag) a segue from the button that
is to summon the popover to the view controller that is to be the UIPopoverController’s
contentViewController and specify “popover” as the segue type. The result is a popover
segue.
A popover segue, when it is triggered, will be an instance of UIStoryboardPopoverSegue.
This is a subclass of UIStoryboardSegue, and it has a popoverController property. The
480
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info
segue, as it is triggered, creates the UIPopoverController for you, and initializes it with
the contentViewController you specified by drawing the segue in the first place. You
can then retrieve this UIPopoverController as the segue’s popoverController in your
implementation of prepareForSegue:sender:. The triggered segue also presents the
popover for you, calling the version of presentPopover... appropriate to the type of
object at the source of the segue (a normal button or a bar button item).
I must warn you that I do not recommend creation of a popover controller using this
approach. If you prefer using a storyboard rather a .xib file, you can certainly configure
the view controller in a storyboard, but then I recommend that you instantiate it as
needed by calling instantiateViewControllerWithIdentifier:. In any case, I suggest
that you create and configure the popover controller, and summon the popover itself,
entirely in code, avoiding popover segues; they are simple, but they can easily become
more trouble than they are worth, and for any serious use they won’t represent any
savings over popover creation in code.
Managing a Popover Controller
Unlike a presented view controller or a child view controller, a UIPopoverController
instance that you create in code is not automatically retained for you by some presenting
view controller or parent view controller just because you have displayed its popover.
You must retain it yourself. If you fail to do so, then if the UIPopoverController goes
out of existence while its popover is on the screen, your app will crash (with a helpful
message: “-[UIPopoverController dealloc] reached while popover is still visible”).
In any case, you might need the retained reference to the UIPopoverController later,
when the time comes to dismiss the popover. Indeed, the entire question of how you
want and expect this popover to be dismissed can require some careful planning, as I’ll
explain further in a moment. There are actually two ways in which a popover can be
dismissed:
The user can tap outside the popover
The user will have a natural expectation of being able to do this. As I’ll explain,
however, you can configure exactly where a tap outside the popover will have the
effect of dismissing it.
You can explicitly dismiss the popover, in code
In order to dismiss the popover explicitly, you send its UIPopoverController the
dismissPopoverAnimated: message. (That’s what I do with the first popover in
Figure 9-1 when the user taps the Done button or the Cancel button.) Obviously,
then, you need a reference to the UIPopoverController!
Even if a popover is normally dismissed automatically by the user tapping outside it,
you still might want to dismiss it explicitly sometimes — so you still might need a ref‐
erence to the popover controller.
Summoning and Dismissing a Popover
www.it-ebooks.info
|
481
A UIPopoverController that is created automatically by triggering a UIStoryboard‐
PopoverSegue, on the other hand, does not have to be retained by your code; the segue,
which is itself retained by the runtime, retains the popover controller. Nevertheless, as
I’ve just said, you still might need a reference to the popover controller, in order to
dismiss it later.
The obvious solution is an instance variable or property with a strong (retain) policy.
In the case where you create the popover controller explicitly, you should immediately
assign it to such a property, so that it will persist as long as the popover is showing. In
the case where the popover controller is created for you by a segue, you should imple‐
ment prepareForSegue:sender: to grab a reference to the segue’s popoverController and assign it to the property.
If, over the lifetime of the app, we’re going to be displaying more than one popover, how
many UIPopoverController properties do we need? In my view, one is enough. A wellbehaved app, in accordance with Apple’s interface guidelines, is never going to display
more than one popover simultaneously. Thus, this one property can be handed a ref‐
erence to the current popover controller each time we present a popover; using that
reference, we will be able later to dismiss the current popover. In fact, I usually call this
property something like currentPop.
Dismissing a Popover
In my earlier discussion of preparing a popover, I omitted one of the most important
aspects of popover configuration — whether, and to what extent, the user is to be per‐
mitted to operate outside the popover without automatically dismissing it. Two prop‐
erties are involved in determining this:
UIPopoverController’s passthroughViews property
This is an array of views in the interface behind the popover; the user can interact
normally with these views while the popover is showing, and the popover will not
be dismissed. What happens if the user taps a view that is not listed in the
passthroughViews array depends on the modalInPopover property.
UIViewController’s modalInPopover property
If this is YES for the popover controller’s view controller (or for its current child
view controller, as in a tab bar interface or navigation interface), then if the user
taps outside the popover on a view not listed in the popover controller’s
passthroughViews, nothing at all happens (the popover is not dismissed).
If it is NO (the default), then if the user taps outside the popover on a view not listed
in the popover controller’s passthroughViews, the view tapped on is unaffected,
and the popover is dismissed.
482
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info
The claim made by the documentation (and by some earlier editions
of this book), that modalInPopover prevents all user interaction out‐
side a popover, is wrong. The user can still interact with a view listed
in the passthroughViews, even if modalInPopover is YES.
You should give attention to your popover controller’s passthroughViews, as the default
behavior may be undesirable. In particular, if a popover is summoned by the user tapping
a UIBarButton item in a toolbar using presentPopoverFromBarButtonItem:..., the
entire toolbar is a passthrough view. This means that the user can tap any button in the
toolbar — including the button that summoned the popover in the first place! The user
can thus by default summon another copy of the same popover while this popover is
already showing, which is certainly not what you want. I like to set the passthroughViews to nil; at the very least, while the popover is showing, you should probably disable
the UIBarButtonItem that summoned it.
Getting the timing right on setting a UIPopoverController’s passthroughViews is not
easy. It might not have any effect unless the UIPopoverController has already been sent
presentPopover.... This is one of the reasons I dislike popover segues; you don’t get
an event after the segue presents the popover, so there’s no good moment to set the
passthroughViews to nil — and, although you can set the passthroughViews in the nib
editor, you can’t set them to nil there.
We are now ready for a rigorous specification of the two ways in which a popover can
be dismissed:
• The popover controller’s view controller’s modalInPopover is NO, and the user taps
outside the popover on a view not listed in the popover controller’s passthroughViews.
The UIPopoverController’s delegate (adopting the UIPopoverControllerDelegate
protocol) is sent popoverControllerShouldDismissPopover:; if it doesn’t return
NO (which might be because it doesn’t implement this method), the popover is
dismissed, and the delegate is sent popoverControllerDidDismissPopover:.
• The UIPopoverController is sent dismissPopoverAnimated: by your code; the
delegate methods are not sent in that case.
If you’re using a popover segue, an unwind segue (see Chapter 6) in the popover
segue’s destination view controller will, when triggered, send dismissPopoverAnimated: to the popover controller for you.
Because a popover can be dismissed in two different ways, if you have a cleanup task to
perform as the popover vanishes, you may have to see to it that the task is performed
under two different circumstances. This can get tricky.
Summoning and Dismissing a Popover
www.it-ebooks.info
|
483
To illustrate, I’ll describe what actually happens in my LinkSame app when the first
popover in Figure 9-1 is dismissed. Within this popover, the user is interacting with
several settings in the user defaults. But if the user taps Cancel, or if the user taps outside
the popover (which I take to be equivalent to canceling), I want to revert those defaults
to the way they were before the popover was summoned. Therefore, as I initially present
the popover, I preserve the relevant current user defaults through a property:
// save defaults so we can restore them later if user cancels
self.oldDefs =
[[NSUserDefaults standardUserDefaults]
dictionaryWithValuesForKeys: @[@"Style", @"Size", @"Stages"]];
The user now works within the popover. Any settings that the user changes within the
popover are immediately saved into the user defaults. So, if the user then taps Done, the
user’s settings within the popover have already been saved; I explicitly dismiss the po‐
pover and proceed to initiate the new game that the user has asked for:
- (void) saveNewGame: (id) sender { // done button in New Game popover
[self.currentPop dismissPopoverAnimated:YES];
self.currentPop = nil;
// ... set up new game interface, initialize scores, etc. ...
}
On the other hand, if the user taps Cancel, I must revert the user defaults as I dismiss
the popover:
- (void) cancelNewGame: (id) sender { // cancel button in New Game popover
[self.currentPop dismissPopoverAnimated:YES];
self.currentPop = nil;
[[NSUserDefaults standardUserDefaults]
setValuesForKeysWithDictionary:self.oldDefs];
}
So far, so good. But we have not yet covered every case. What if the user taps outside
the popover to dismiss it? I take that to mean Cancel (the user did not tap Done, after
all). Therefore I implement the delegate method to detect this, and I revert the user
defaults here as well:
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)pc {
[[NSUserDefaults standardUserDefaults]
setValuesForKeysWithDictionary:self.oldDefs];
self.currentPop = nil;
}
But wait — there’s more. My app has another popover (the second popover in
Figure 9-1). This popover, too, can be dismissed by the user tapping outside it; in fact,
that’s the only way the user can dismiss it. The popover controllers for both popovers
have the same delegate. Thus, when the second popover is dismissed, the same popoverControllerDidDismissPopover: implementation will be called. But now we don’t want
484
|
Chapter 9: Popovers and Split Views
www.it-ebooks.info