1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

Chapter 9. Popovers and Split Views

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



Xem Thêm
Tải bản đầy đủ (.pdf) (929 trang)

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

Tải bản đầy đủ ngay
×