iPad开发之--splitView和popover

iPad上的app与iPhone上的app最显著的不同就是iPad使用splitView和popover来代替navigationController

在介绍正文之前,先来谈一谈如何实现进入不同的view出现不同的toolbars?

关于Navigation Controller有一点很重要,toolbar上的按钮和navigationController本身没有任何关系,它只和当时显示的viewController有关。我们现在知道如何使用push segues来push新的东西到navigationController,所以当你slide进来新事物时,新的按钮会出现在底部栏上

UIViewController有一个方法可以重载,或者你可以直接设置toolbarItems,因为实际上它是一个property,它是一个UIBarButtonItems的NSArray。只要你的viewController被push到一个navigationController,并且这个navigationController底部有设置toolbar,那么你就会看到你的按钮

现在让我们讲一下UIBarButtonItems,真正使得UIToolBar工作的东西

他们有target action,就像一个按钮,这是当点击他们时,你如何找到它。他们可以有边框或者简单朴素的背景。你可以像施膜法一样设置它的标题,你可以设定一个图片,比如你设计的图标什么的;或者你甚至可以设置一个你自定义的view;此外,还有不少系统内置的。

然后这有两个特殊的UIBarButtonItems,Fixed和Flexible space,这些都是用来安排你UIToolbar按钮的显示方式

创建一个UIBarButtonItem的方式是alloc/ init,他可以采用文字或图像;或者另外一种初始化叫initWithBarButtonSystemItem,然后你给它在枚举类型中指定一个,详情参见苹果文档,嘿嘿嘿

因此创建一个工具栏超级简单,你只需要将其拖动到你的viewController,通常你会连到一个outlet,然后你只需要从Xcod拖个UIBarButtons到它里面;或者在代码中,你可以设置个数组,工具栏对象有个物品数组,就是仅仅只要一个数组

splitView

同样的,创建一个splitViewController的方法是在xcode里将它拖出来,你只能拖动spiltView到一个iPad类型的storyboard。splitView通常只是一个基本元素,它填满了整个屏幕,你不可能把splitView放到navigationController或popover或其他什么的内部。一般情况下,splitView是提供给整个app的。

正如你看到的splitView,有两个View controllers,一个左侧一个右侧。我们叫左侧master,右侧detail。你get和set这两个东西超级简单,splitViewController有一个property叫做viewControllers,他是一个数组,这个数组只用两个东西,左边和右边,左侧是元素0,右侧是元素1。这是一个可设置的东西,所以你可以在代码中设置你的两个view controllers。注意一下,通常使用这个API使你得到同时设置这两个东西。但是当然你可以先get它,使其可变,只设置其中一个,然后在设置回去,虽然Xcode会尽量避免你这么做

你会注意到,辅助API的信息,它是外部property的non-atomic copy,不是strong的。这是为什么?这个API不希望你传递含有这两个view controllers的可变数组,然后期待如果你改变了一个,它会以某种方式更新splitView,所以,这里说的是我要复制的东西,而且我要使用它。所以如果你要改变它,你得再给我一次。这就是我们使用复制的原因。也正是因为如此,你会发现iOS里NSArray和NSString的copy用法相当普遍,他们就是不想让你传递可变数组,以防止你改变了它导致发生意外。

splitView不能没有delegate,这是iOS对象通过delegate来完成自己的工作。如果你不设置delegat,那么当splitView进入Portrait模式的时候,左侧会消失,你应该在角落里放一个小按钮,使用户可以点击它来让左侧出现在Popover里,这就是splitView的工作原理。但是如果你不实现delegat你就没办法按上面那个按钮,这样就会导致你进入一个尴尬的境地,你无法在Portrait模式下调出左侧。

通常情况下,你要在你的viewController的方法viewDidLoadawakeFromNib里设置此delegate(awakeFromNib会被发送给每一个从storyboard里出来的对象,你能通过UIView看到它,它就能接受awakeFromNib)。同样的,我们从initWithFrame和awakeFromNib调用Setup。

因此,viewControllers也接受awakeFromNib。此外,当awakeFromNib被调用时,这会是非常早期,就是它刚从storyboard出来的时候。如果你的splitViewController是被control拖入Xcode的,那么它会和你的viewController关联在一起。因此,awakeFromNib是另一个地方。
splitView的delegate的主要任务就是处理旋转,你有了这个splitView,它在屏幕上了,如果你处在landscape模式并然后你去到Portrait,会发生一些改变。

delegate控制一切,并完成button dance,这里叫button dance是因为你需要挑一个按钮放到工具栏中,当旋转到另个方向,把他拿出去,当它旋转回Portrait时,再放回来
@property (nonatomic, assign) id delegate;
注意这里是assign,assign和weak差不多,但是更坏一点,因为它不作零处理了。所以你要小心一些,你创建的不是一个strong指针delegate,因此如果当assign从堆上释放,你会得到一个野指针,因为他不是weak。

就像上文所说,右侧是detailed的一侧,在splitViewController里它总是可见的,无论怎么旋转或者你的delegate是做什么的,它总是可见的。因此,delegate才是管理左侧的。
这里有一个方法

1
2
3
4
5
- (BOOL)splitViewController:(UISplitViewController *)sender shouldHideViewController:(UIViewController *)master inOrientation:(UIInterfaceOrientation)orientation {
//return YES; //always hide it
//return NO; //always display it
//return UIInterfaceOrientationIsPortrait(orientation); //这是默认的情况,当landscape就显示,portrait就隐藏
}

这种方法是被发送到你的delegate询问在特定方向下你想要左侧做什么,因此它把自己传递给你,在data source方法里,第一个参数几乎总是sender,然后在传递给你左侧,因为他想要隐藏左侧。这里有三种选择,你可以一直说YES,这意味着左侧一直不会出现在屏幕上,他总是藏在按钮的背后,换言之,你只能通过点击角落里的按钮来查看左侧,它将出现在一个Popover里;你可以说NO,这意味着左侧和右侧都始终出现在屏幕上;如果你不实现这个方法系统会做的默认的选择,但是这样出现其他的问题,因为portrait会隐藏,但你没有那个小按钮,系统不会帮你做到这步,它会给你按钮,但你需要把它放到屏幕上。

如果你忘记设置了delegate,会在portrait中出现这种情况:在popover的左侧并没有出现按钮,因此,其余的splitViewDelegate方法就是把按钮放上去

1
2
3
4
5
6
- (void)splitViewController:(UISplitViewController *)sender willHideViewController:(UIViewController *)master withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popover {
barButtonItem.title = @“master”; //use a better word than “master”
//setSplitViewBarButtonItem: must put the bar button somewhere on screen
//probably in a UIToolbar or a UINavigationBar
[detailViewController setSplitViewBarButtonItem: barButtonItem];
}

有两个是非常重要的,一个把按钮放上去,一个把按钮拿掉。这是用来隐藏左侧的,它会给你一个barButton,他会把第三个参数barButton放到屏幕上,当这个barButton被按下,他会自动显示左侧

1
2
3
4
- (void)splitViewController:(UISplitViewController *)sender willHideViewController:(UIViewController *)master withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popover {
//removeSplitViewBarButtonItem: must remove the bar button from its toolbar
[detailViewController removeSpiltViewBarButtonItem: nil];
}

相反,当你想要旋转回landscape,并把左侧放回到屏幕上,它会向你发送此消息invalidatingBarButton,换句话说,把它从你的工具栏中移除

1
2
3
4
5
6
7
8
9
10
11
- (void)setSpiltViewBarButtonItem:(UIBarButtonItem *)barButtonItem {
UIToolbar *toolbar = [self toolbar]; //might be outlet or calculate
NSMutableArray *toolbarItems=[toolbar.items mutableCopy];
if (_splitViewBarButtonItem)
[toolbarItems removeObject:_spiltViewBarButtonItem];
//put the bar button on the left or our existing toolbar
if (barButtonItem)
[toolbarItems insertObject: barButtonItem atIndex: 0];
toolbar.items = toolbarItem;
_spiltViewBarButtonItem = barButtonItem;
}

这是首先要做的,有很多方法实现splitViewsDelegates,这一切取决于你在两侧使用的是什么view controllers

何时你该设置你的delegate?

一是你需要决定谁是delegate,是左侧的master,还是右侧的detail。他们都是合理的选择,这要依情况而定。此外你需要去考虑重用性,两者之一可能是通用的,所以你不能再那上面实现splitViewDelegate,它是一个通用的可重复使用的view,但你可以先subclass它,然后再去做。但是另一个可能是专门用在你的app上的,所以把它放在那比较有意义

所以现在你有master和detail,他们之所以这么叫是因为当你点击master里面的东西是,detail会显示你点击的东西的详细信息。

那么,当master改变时,detail是怎么更新的?

答案是有两个选择。
一是简单的target action,在左侧的masterView你已经有了一些按钮或者什么的,当其被点击时,它发送target action消息给你的masterController,当它想更新detail时只需要发一个消息到detail,现在将告诉你它是如何从master得到detail的,很简单
例如,masterViewController里一个target action消息叫doit,它就会使用UIViewController一个特殊的方法self.splitViewController来得到detailController。

1
2
3
4
- (IBAction)doit {
id detailViewController = [[self.spiltViewController viewControllers] lastObject];
[detailViewController setSomeProperty: …];
}

这是一个UIViewController的property,他总是返回splitViewController,前提是你所在的是一个splitViewController

Q:self.navigationController和self.splitViewController都可以返回非nil吗?
A:当然可以。因为你自己是master,你可以在splitViewController里有一个navigationController,因此,self.navigationController将返回到你所在的navigationController;self.splitViewController将返回你所在的splitViewController。即使你在navigationController里并且同时你离开当前屏幕,你还是在splitViewController里,他仍然会返回非nil。这和是否在屏幕上无关,和是否在UI结构里有关。如果你在navigationController里,你要返回的话,back按钮会找到你 -> 你仍然在splitView里

你通过splitViewController的viewController数组的最后一个对象来获得detailViewController,记住该数组中只有两样东西,所以你可以获取最后一个,然后你要设置它的一些properties,那么他就会更新,就是这样,没有其他你需要做的了,setSomeProperty会有方法来自我更新,比如setNeedsDisplay什么的。

二是你也可以使用segue,这样稍微复杂一点。在splitViewController里只有一种segue能用,它被称为replaceSegue。这是因为它会替换detail或master。用segue替换master非常罕见,但是让我们把重点放在有关替换detail上。

有一个情况是你想要替换detailView为新的带按钮的,然后如果你点击另一个按钮,在替换回来。所以这就是用到replace的地方。这有一个有趣的警告关于replace的,当你替换detailView时,如果工具栏里有splitViewBarButton,当然该工具栏也被替换,因为整个view都会替换,所以你需要把splieBarButton转移出来,你需要将它转移到新的delegate生成的popover上,你通常在准备segue时完成去做这些。

所以如果你需要准备segue,这是一个replaceSegue,你需要问detail关于它的barButton,你设置barButton,然后让segue发生。

splitView就说这么多

popover

就是漂浮选单

iOS里有一个UIPopoverController类,这不是一个UIViewController,这不是UIPopoverViewController,因此他只是一个NSObject,它控制viewController。

它的工作基本上就是通过contentViewController绘制它的viewController,这是UIPopoverController的property。你设定好了之后它会控制一个包含该viewController的矩形,一个小箭头指向把它画出来的东西。

通常上,popover的发生是因为你control拖动它,并有一个segue,这样就有了一个popoverSegue。popoverSegue做的是,你control拖动它到viewController,它会把viewController放到popover里,不管你是从按钮或什么拖动,那就是popover的指向。

当你get popoverSegue的prepareForSegue方法时,其参数是一个UIStoryboardPopoverSegue类,所以你必须假设它总是一个popoverSegue。然后我建议你做一个cast,这样你就可以访问该segue的property和popoverController。

你可以不通过segue把popover放到屏幕上,你可以alloc/ initWithContentViewController,给它你想要放进popover的viewController,然后把它放到屏幕上。

你也可以通过- (BOOL)popoverVisible;来问popoverController当前popover是否可见。

因此,如果你在代码中做popover,或者使用了storyboard,在storyboard的方法instantiateViewControllerWithIdentifier会给你一个storyboard外部的viewController。

所以无论使用了那种方法,如果你得到一个viewController然后发消息给UIPopoverController,进行alloc/ init,那么现在你手上有了个popoverController,你向其发送这两个消息之一,使其出现在屏幕上

1
- (void)presentPopoverFromRect:(CGRect)aRect or inView:(UIView *)view permittedArrowDirections:(UIPopoverArrowDirection)direction animated:(BOOL)flag;

或者这样

1
- (void)presentPopoverFromBarButtonItem:(UIBarButtonItem *)barButtonItem permittedArrowDirections:(UIPopoverArrowDirection)direction animated:(BOOL)flag;

这就是你不使用segue的情况。你在一个小的矩形区域内显示popover,也许是一些选定的文本或者view或按钮一类的。或者,你可以在一个barButton显示它,点击barButton后出现popover很常见,所以你只需要传递barButton。

注意这个参数,permittedArrowDirections。你也许会问,如果从popover或者barButton或一个矩形区域调用,它会被放在屏幕上哪里?答案是他可以放在任何可以容得下它的地方,但是它会尽量使用你的permittedArrowDirections来决定是否要放到左边或下边。这样,如果你不允许它的箭头向左,那么它就会试着放到下边或者其他方向;如果你无法找到合适的地方,系统会把它放到屏幕上的某个地方,屏幕上总会有放得下的地方。

有个微妙的事情要真正小心,你需要保持一个strong指针指向你的popoverController,所有这些显示的东西都不会有strong指针指向你的popoverController。所以会有很多人写这样的代码:

1
2
3
4
- (IBAction)presentPopover {
UIPopoverController *pop = [[UIPopoverController alloc] initWithViewController: vc];
[pop presentPopoverFromBarButtonItem: item permittedArrowDirections: …];
} //BAD! No strong pointer to the popover controller! presenting it is not enough!

他们会说写到这里方法就结束了,然后他们的程序就会崩溃,崩溃的原因就是因为这个UIPopover,它唯一的strong指针是这个本地变量UIPopoverController pop,当出了这个方法的范围,没有人有strong指针指向这个UIPopoverController了。所以当你使用popoverController时,你几乎总是在你的对象里创建一个strong property -> `@property (nonatomic, strong) UIPopoverController myPopover`

现在你的屏幕上已经有了一个popover,现在要消除它,把它从屏幕上去掉。这个动作通常是用户触发的。但是他们可能有两种方式。

一是如果他们点击了屏幕上除popover以外的地方,它会关闭popover,除非点击的是@property (nonatomic, copy) NSArray *passthroughViews;中的一个。换句话就是说,可以在popover以外的特定地方,即使用户点击了,也不会消除popover,你甚至可以拓展到整个屏幕,那么用户只能通过popover里的东西来消除popover。

现在,当你点击popover里的东西,它是怎么消除的呢?答案是调用了- (void)dismissPopoverAnimated:(BOOL)animated;。现在另一个精妙的地方是,你不能从popover里的viewController调用该方法,虽然这么做很有诱惑力。在viewController里有个“ok”按钮,你点击它来调用上诉方法。

不管吧popover放上来的是哪个对象,这个对象就应该负责消除popover。那么怎么在popover里放一个ok按钮呢?答案是大量使用delegation。popover内的viewController通常有个property是他的delegate,当ok键被按下,它会发消息给它的delegate,然后谁把它放上来就要去消除它。

这样看起来多了好多额外的间接操作,我创建此delegate只是为了说“ok,消除它”,但是这样会更加面向对象,因为它使popover里面的东西不用知道他是在popover里面,他可以用其他方式显示,它可以被pushed进navigationController,他可以在形态上显示,以其他方式重用。这一点也许你会觉得混乱,但是它是必须了解的,你要记住它。

所以说了这么多,就是为了说明谁放的,谁就负责消除。

popover有delegate,但用处不多,它主要做的是告诉你关于消除的事。他实际上有两个方法,一是问你是否消除,另一个是告诉你他何时被消除。但是这只发生在有人通过点击popover以外的地方来消除的情况,如果是调用dismissPopoverAnimated你就不会用到这个。
所以这是用户消除popoverController

popover的大小

popover在屏幕上,他会有多大?

有三种方式设置大小。

一是在Xcode里设置,如果你选中一个viewController并inspect它,在inspector的底部,你会看到有一个小开关叫popover,如果你切换到on,并表明明确使用的大小,由你指定当viewController处在popover里时的大小。

但是会有一些小限制,有时viewController所在的popover够大而有时会太小,所以UIViewController有一个方法- (CGSize)contentSizeInPopover;,这是一个UIViewController的方法,你重载它,返回一个CGSize的大小,他可以计算出大小。

第三种方法是UIPopoverController有个方法可以设置popover的大小,- (void)setPopoverContentSize:(CGSize)size animated:(BOOL)animated;

这些就是关于iPad开发的基础内容。