Tải bản đầy đủ - 0 (trang)
Chapter 22. Popovers and Split Views

Chapter 22. Popovers and Split Views

Tải bản đầy đủ - 0trang

Figure 22-1. Two popovers



• Both views continue to appear side by side; the second view is narrower, because

the screen is narrower. (This option is new in iOS 5.)

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

(Chapter 21). 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 (and it is no coincidence that its first view is sized to hold the master table that occupied the entire screen

on the iPhone).

Split views were important before iOS 5, because UISplitViewController was the only

legal way in which a single view controller could display the views of two child view

controllers side by side. In iOS 5, however, where you are free to design your own

custom parent view controllers, UISplitViewController will probably diminish in value.

Apple’s Mail app on the iPad used a UISplitViewController in iOS 3.2 and iOS 4, but

in iOS 5, it uses a different interface (which I’ll try to reverse-engineer later in this

chapter).



650 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



Configuring and Displaying a Popover

To display a popover, you’ll need a UIPopoverController, along with a view controller

(UIViewController) whose view the popover will contain. UIPopoverController is not

itself a UIViewController subclass. The view controller is the UIPopoverController’s

contentViewController. You’ll set this property initially through UIPopoverController’s designated initializer, initWithContentViewController:. Later, 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 22-1 is initialized.

I have a UIViewController subclass, NewGameController. NewGameController’s view

contains a table (Figure 21-2) and a UIPickerView (Chapter 11), and is itself the data

source and delegate for both. I instantiate NewGameController and use this instance

as the root view controller of a UINavigationController, giving its navigationItem a

leftBarButtonItem (Done) and a rightBarButtonItem (Cancel). I don’t really intend to

do any navigation; I just want the two buttons, and this is an easy way of getting them

into the interface. That UINavigationController then becomes a UIPopoverController’s

view controller:

NewGameController* dlg = [[NewGameController alloc] init];

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];



Note that that code doesn’t cause the popover to appear on the screen! I’ll come to that

in a moment.

The popover controller should also be told the size of the view it is to display, which

will be the size of the popover. The default popover size is {320,1100}; Apple would

like you to stick to the default width of 320 (the width of an iPhone screen), but a

maximum width of 600 is permitted, and the second popover in Figure 22-1 uses it.

The popover’s height might be shorter than requested if there isn’t enough vertical

space, so the view to be displayed needs to be prepared for the possibility that it might

be resized.

You can provide the popover size in one of two ways:



Configuring and Displaying a Popover | 651



www.it-ebooks.info



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 contentSizeForViewInPopover property

The UIViewController 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 UIViewController will generally

know its own view’s ideal size. If a view controller is instantiated in a nib or storyboard, this value can be set in the nib or storyboard.

In the case of the first popover in Figure 22-1, the NewGameController sets its own

contentSizeForViewInPopover in viewDidLoad; its popover size is simply the size of its

view:

self.contentSizeForViewInPopover = self.view.bounds.size;



The popover itself, however, will need to be somewhat taller, because the NewGameController is embedded in a UINavigationController, whose navigation bar occupies

additional vertical space. Delightfully, the UINavigationController takes care of that

automatically; its own contentSizeForViewInPopover adds the necessary height to that

of its root view controller.

In case of a conflict, the rule seems to be that 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, the change is obeyed; specifically, my experiments suggest that if the UIViewController’s contentSizeForViewInPopover is changed (not merely set to the value it already has), the UIPopoverController

adopts that value as its popoverContentSize and the popover’s size is adjusted accordingly.

If a popover’s contentViewController is a UINavigationController, and a view controller is pushed onto or popped off of its stack, then if the current view controller’s contentSizeForViewInPopover 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

workaround is to implement the UINavigationController’s delegate method

navigationController:didShowViewController:animated:, so as to set the navigation

controller’s contentSizeForViewInPopover explicitly:

- (void)navigationController:(UINavigationController *)navigationController

didShowViewController:(UIViewController *)viewController

animated:(BOOL)animated {

navigationController.contentSizeForViewInPopover =

viewController.contentSizeForViewInPopover;

}



652 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



(That workaround is not entirely satisfactory from a visual standpoint, as two animations succeed one another, but I tried implementing willShowViewController... instead

and liked the results even less.)

The popover is made to appear on screen by sending the UIPopoverController one of

the following messages (and the UIPopoverController’s popoverVisible property then

becomes YES):

• presentPopoverFromRect:inView:permittedArrowDirections:animated:

• presentPopoverFromBarButtonItem:permittedArrowDirections:animated:

The popover has a sort of triangular bulge (called its arrow) on one edge, pointing to

some region of the existing interface, from which the popover thus appears to emanate

and to which it seems to be related. The difference between the two methods lies only

in how this region is specified. 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. Starting in iOS 5, however, you do get some veto power: set the

UIPopoverController’s popoverLayoutMargins to a UIEdgeInsets stating the margins,

with respect to the root view bounds, within which the popover must appear. If an 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 should try not to do that.

Starting in iOS 5, you can customize much of a popover’s appearance. For example,

the first popover in Figure 22-1 has a dark navigation bar even though no such thing

was requested when the UINavigationController was created. This is because a popover

whose content view controller is a navigation controller likes to take control of its

navigation bar’s barStyle and set it to a special undocumented style, evidently to make



Configuring and Displaying a Popover | 653



www.it-ebooks.info



Figure 22-2. A very silly popover



it harmonize with the popover’s border. There isn’t much you can do about this: setting

the navigation bar’s tintColor has no effect, and setting its backgroundColor is effective

but ugly. But in iOS 5, you can take charge of a UINavigationBar’s background image

and you can customize the position and appearance of its bar button items (Chapter 25), so you can achieve some pleasing results.

Moreover, in iOS 5 you can customize the outside of the popover — that is, the “frame”

and the arrow. To do so, you set the UIPopoverController’s popoverBackgroundViewClass to your 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 22-2. You get to dictate (by implementing contentViewInset) the

thickness of all four sides of the “frame”.

In my experiments, I found it necessary to import into the header file of my UIPopoverBackgroundView subclass. This feels like a bug; to subclass a class that is part of

UIKit, importing UIKit should be sufficient.



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

direction, which you can learn from the UIPopoverBackgroundView’s arrowDirection property. I’ll give a simplified example in which I assume that the arrow

direction will be UIPopoverArrowDirectionUp. Then drawing the “frame” is easy: here,

654 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



I divide the view’s overall rect into two areas, the arrow area on top (its height is a

#defined constant, ARHEIGHT) and the “frame” area on the bottom, and draw the “frame”

into the bottom area as a resizable image (Chapter 15):

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)];

CGRect arrow;

CGRect body;

CGRectDivide(rect, &arrow, &body, ARHEIGHT, CGRectMinYEdge);

[lin drawInRect:body];



The documentation claims that the popover will be awarded a shadow automatically,

but in my tests this was not the case. So I also apply my own shadow around the “frame”

shape:

self.layer.shadowPath = CGPathCreateWithRect(body, NULL);

self.layer.shadowColor = [UIColor grayColor].CGColor;

self.layer.shadowRadius = 20;

self.layer.shadowOpacity = 0.4;



Now for the arrow. The UIPopoverBackgroundView has an arrowHeight property and

an arrowBase property that you’ve set to describe the arrow dimensions to the runtime.

(In my code, their values are provided by two #defined constants, ARHEIGHT and

ARBASE.) My arrow will consist simply of a texture-filled isosceles triangle, with an excess

base consisting of a rectangle to make sure it’s attached to the “frame”. (In my real code

I draw the arrow first, so that its excess is behind 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 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);



Configuring and Displaying a Popover | 655



www.it-ebooks.info



CGContextAddRect(con, CGRectMake(0,ARHEIGHT,ARBASE,15));

CGContextClip(con);

[lin drawAtPoint:CGPointMake(-40,-40)];

CGContextRestoreGState(con);



Managing a Popover

Unlike a presented view controller or a child view controller, a UIPopoverController

instance is not automatically retained for you by some presenting view controller or

parent view controller; you must retain it yourself. If you fail to do this, 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”). Also, you might need the retained reference to the UIPopoverController later, when the time comes to dismiss the popover.

There are actually two ways in which a popover can be dismissed: the user can tap

outside the popover, or you can explicitly dismiss the popover (as I do with the first

popover in Figure 22-1 when the user taps the Done button or the Cancel button). In

order to dismiss the popover explicitly, you send its UIPopoverController the dismissPopoverAnimated: message. 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 on certain occasions — so you still might

need a reference to the popover controller. For example, in keeping with the transient

nature of popovers, I like to dismiss the current popover programmatically when the

application undergoes certain strong transitions, such as going into the background or

being rotated. (See also Apple’s technical note on what to do when the interface rotates

while a popover is showing, QA1694, “Handling Popover Controllers During Orientation Changes.”) You can listen for the former by registering for UIApplicationDidEnterBackgroundNotification, and for the latter by implementing willRotateToInterfaceOrientation:duration:. This policy is not merely aesthetic; some view controllers, especially certain built-in specialized view controllers, recover badly from such

transitions when displayed in a popover.

The obvious solution is an instance variable or property with a strong (retain) policy.

The question then is how many such instance variables to use if the app is going to be

displaying more than one popover. We could have one instance variable for each popover controller. On the other hand, a well-behaved app, in accordance with Apple’s

interface guidelines, is probably never going to display more than one popover simultaneously; so a single UIPopoverController instance variable (we might call it currentPop) should suffice. This one instance variable could be handed a reference to the current popover controller each time we present a popover; using that reference, we would

be able later to dismiss the current popover and release its controller.



656 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



Dismissing a Popover

An important feature of a popover’s configuration is whether and to what extent the

user can operate outside it without automatically dismissing it. There are two aspects

to this configuration:

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), the popover is

absolutely modal; any tap outside it will be ignored and won’t have any effect at

all, not even to dismiss the popover. The default is NO.

UIPopoverController’s passthroughViews property

This matters only if modalInPopover is NO. It is an array of views in the interface

behind the popover; the user can interact with these views, but a tap anywhere else

outside the popover will dismiss it (with no effect on the thing tapped). If

passthroughViews is nil, a tap anywhere outside the popover will dismiss it.

Setting a UIPopoverController’s passthroughViews might not have any

effect unless the popover is already showing (the UIPopoverController

has been sent presentPopover...).



A popover can present a view controller internally; you’ll specify a modalPresentationStyle of UIModalPresentationCurrentContext, because otherwise the presented view

will be fullscreen by default. You’ll also specify a transition style of UIModalTransitionStyleCoverVertical — with any other transition style, your app will crash with this

message: “Application tried to present inside popover with transition style other than

UIModalTransitionStyleCoverVertical.” The presented view controller’s modalInPopover is automatically set to YES: that is, while the presented view controller is being

presented within the popover, the user can’t make anything happen by tapping outside

the popover, not even to dismiss the popover. (You can subvert this by setting the

presented view controller’s modalInPopover to NO after it is presented, but you probably

shouldn’t.)

If modalInPopover is NO, you should pay attention to the passthroughViews, as the default behavior may be undesirable. For example, 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. The user can thus by

default summon the popover again while it is still 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 the popover.



Dismissing a Popover | 657



www.it-ebooks.info



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. Typically this would be because you’ve

included some interface item inside the popover that the user can tap to dismiss

the popover (like the Done and Cancel buttons in the first popover in Figure 22-1).

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 this task is performed

under two different circumstances. That can get tricky.

To illustrate, I’ll describe what happens when the first popover in Figure 22-1 is dismissed. Within this popover, the user is interacting with several settings in the user

defaults. But if the user cancels, 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. So, as I initially present the popover, I preserve the relevant

current user defaults as an ivar:

// save defaults so we can restore them later if user cancels

self.oldDefs = [[NSUserDefaults standardUserDefaults] dictionaryWithValuesForKeys:

[NSArray arrayWithObjects:@"Style", @"Size", @"Stages", nil]];



Now, if the user taps Save, the user’s settings within the popover have already been

saved (in the user defaults), so I explicitly dismiss the popover and proceed to initiate

the new game that the user has asked for. 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];

}



But I must also do the same thing if the user taps outside the popover. Therefore I

implement the delegate method and revert the user defaults there as well:

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)pc {

[[NSUserDefaults standardUserDefaults]

setValuesForKeysWithDictionary:self.oldDefs];

self.currentPop = nil;

}



658 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



There is a problem with the foregoing implementation, however. My app, you may

remember, has another popover (the second popover in Figure 22-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. This means that popoverControllerDidDismissPopover: will be called.

But now we don’t want to call setValuesForKeysWithDictionary:; it’s the wrong popover, and we have no preserved defaults to revert. This means that I must somehow test

for which popover controller is being passed in as the parameter to popoverControllerDidDismissPopover:. But how can I distinguish one popover controller from another?

Luckily, my popover controllers have different types of view controller:

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)pc {

if ([pc.contentViewController isKindOfClass: [UINavigationController class]])

[[NSUserDefaults standardUserDefaults]

setValuesForKeysWithDictionary:self.oldDefs];

self.currentPop = nil;

}



If this were not the case — for example, if I had two different popovers each of which

had a UINavigationController as its view controller — I’d need some other way of

distinguishing them. This is rather a knotty problem, and in the past I’ve resorted to

various desperate measures to resolve it, such as subclassing UIPopoverController.

Earlier, I mentioned that I also want to dismiss any currently displayed popover if the

interface rotates, or if the app goes into the background. That means that I must perform

these same tests again in response to the appropriate notifications:

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)io

duration:(NSTimeInterval)duration {

UIPopoverController* pc = self.currentPop;

if (pc) {

if ([pc.contentViewController isKindOfClass:[UINavigationController class]])

[[NSUserDefaults standardUserDefaults]

setValuesForKeysWithDictionary:self.oldDefs];

[pc dismissPopoverAnimated:NO];

self.currentPop = nil; // wrong in previous version

}

}

-(void)backgrounding:(id)dummy {

UIPopoverController* pc = self.currentPop;

if (pc) {

if ([pc.contentViewController isKindOfClass:[UINavigationController class]])

[[NSUserDefaults standardUserDefaults]

setValuesForKeysWithDictionary:self.oldDefs];

[pc dismissPopoverAnimated:NO];

self.currentPop = nil; // wrong in previous version

}

}



In my view, the need for all this work, all this testing and caution and duplication of

functionality, just to display a couple of popovers, demonstrates that the framework’s

implementation of popover controllers is deeply flawed. You don’t get sufficient help



Dismissing a Popover | 659



www.it-ebooks.info



with getting a reference to a UIPopoverController from a view currently being displayed

within it, with managing a popover controller’s memory, or with distinguishing one

popover controller from another. I’ve shown how you can work around these shortcomings, but in a better world such workarounds wouldn’t be necessary. (For example,

because only one popover is supposed to be showing at a time, the framework could

just maintain a reference to its controller for you.) Popovers were invented for iOS 3.2;

frankly, I’m astonished that we’ve reached iOS 5.0 with no improvement in this area

of the API.



Popover Segues

In an iPad storyboard, a segue can be designated a popover segue, by choosing Popover

from the Style pop-up menu in the Attributes inspector. The consequences of doing so

are:

• When the segue is triggered, a popover is displayed. The runtime constructs a

UIPopoverController and makes the segue’s destination view controller the

UIPopoverController’s view controller. The popover’s “anchor” (the view or bar

button item to which its arrow points) is the source object from which you controldrag to form the segue, or it can be set in the Attributes inspector.

• The segue is a UIStoryboardPopoverSegue, a UIStoryboardSegue subclass that

adds a single read-only property, popoverController. You can use this, for instance

in prepareForSegue:sender:, to customize the popover controller.

The UIPopoverController created by the triggering of a popover segue is retained behind the scenes; the app does not crash if you fail to retain it explicitly yourself. (I don’t

know what object is retaining it, but I think it’s the storyboard.) Nevertheless, you will

still probably want to retain your own reference to the popover controller, because

you’re still likely to need that reference for the reasons discussed in the preceding section. You’ll probably obtain that reference in your prepareForSegue:sender: implementation.

Popover segues sound tempting, but they do not appreciably reduce the amount of

code required to configure and manage a popover. On the contrary, in my experience

they tend to increase it. Consider, for example, the code I cited earlier for creating a

popover controller whose view controller is a navigation view controller:

NewGameController* dlg = [[NewGameController alloc] init];

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:)];



660 | Chapter 22: Popovers and Split Views



www.it-ebooks.info



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 22. Popovers and Split Views

Tải bản đầy đủ ngay(0 tr)

×