自定义 Segue
你已经看过两种类型的 segue 了:Modal 和 Push。这对大部分程序来说足够了,但有时候你可能感到腻味。幸运的是,你可以创建自己的 segue 动画来稍微调剂一下。
让我们来试试如何将从 Gestures 到 Ranking 屏的转换动画定义为自己的动画效果。选中 BestPlayers segue,将它的 Style 设置为Custom。接下来会让你输入 segue 类名,输入“SuperCoolSegue”。同样,在 WorstPlayers segue 上也做一遍。
创建自己的 segue,需要子类化 UIStoryboardSegue 类。添加新的O-C 类,命名为 SuperCoolSegue,继承 UIStoryboardSegue。
在这个类中,我们需要做的就是实现一个 perform 方法:
@implementationSuperCoolSegue
- (void)perform
{
[self.sourceViewController presentViewController:self.
destinationViewControlleranimated:NO completion:nil];
}
@end
在这个方法中我们在源controller 上呈现了目标 ViewController(模式化窗体),不使用任何动画过渡。过去,我们可以使用presentModalViewController:animated:方法,但 iOS5 以后,最好是使用新方法呈现 ViewController。
运行 app。当你在屏幕上划动手势,Ranking 窗口不经任何动画过渡直接出现了。整个过程是如此突兀,我们应该给它加上一点动画效果。
修改 SuperCoolSegue.m为:
#import<QuartzCore/QuartzCore.h>
#import"SuperCoolSegue.h" @implementation SuperCoolSegue
- (void)perform
{
UIViewController*source = self.sourceViewController;
UIViewController *destination= self.destinationViewController;
// 以目标窗口创建 Image
UIGraphicsBeginImageContext(destination.view.bounds.size);
[destination.view.layer renderInContext:
UIGraphicsGetCurrentContext()];
UIImage *destinationImage=
UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 将 Image 加到 tab barcontroller 的 subview
UIImageView *destinationImageView=
[[UIImageView alloc]initWithImage:destinationImage];
[source.parentViewController.viewaddSubview:
destinationImageView];
// 缩小 Image并旋转 180 度 (颠倒)
CGAffineTransform scaleTransform= CGAffineTransformMakeScale(0.1, 0.1);
CGAffineTransform rotateTransform= CGAffineTransformMakeRotation(M_PI);
destinationImageView.transform= CGAffineTransformConcat(scaleTransform,rotateTransform);
// 将 image 移到屏幕之外
CGPoint oldCenter= destinationImageView.center;
CGPoint newCenter= CGPointMake(oldCenter.x -
destinationImageView.bounds.size.width,oldCenter.y);
destinationImageView.center= newCenter;
// 启动动画
[UIView animateWithDuration:0.5fdelay:0 options:UIViewAnimationOptionCurveEaseOut
animations:^(void)
{
destinationImageView.transform = CGAffineTransformIdentity;
destinationImageView.center = oldCenter;
}
completion: ^(BOOLdone)
{
// 移除Image,我们用不到了
[destinationImageView removeFromSuperview];
// 呈现新窗口
[source presentViewController:destinationanimated:NO completion:nil];
}];
}
@end
在开始动画之前,先为新的 ViewController 做一份屏幕拷贝,得到一个UIImage,然后用这个 UIImage 进行动画。也可以直接用 view 进行动画,但效率低而且不一定达到你想要的效果。像 navigationcontroller 这样的控制器在进行这样的操作时不太容易。
我们将 UIImage 添加到 TabBarController 的 subview,将它绘制在视图的最上层。在开始动画之前,UIImage是以缩小和翻转的形式位于屏幕可视区域之外。
动画完成之后,我们会移除 UIImage 并呈现目标ViewController。UIImage 形成的动画和真正的 view 在用户看来天衣无缝,因为二者的显示内容是完全相同的。
顺便多说两句。如果你觉得这个动画效果还不够炫,你完全可以自己换一个。看看你可以使用什么效果……这会是一个有趣的工作。如果你还想对源ViewController 进行动画,我还是建议你从它获得一个 UIImage 来进行。
在你关闭 Ranking 界面时,会显示一个从上滑到底部的默认动画。你仍然可以定制这个动画,但这不属于segue 的一部分,而是由委托对象来负责,上面的原则仍然适用。将 animated 参数设置为 NO,然后定义你自己的动画。
故事板和 iPad
我们打算做一个 universal 版本,以便我们的 app 支持 iPad。
打开 Target 的 Summary 页,在 iOSApplication Target 下找到 Devices 一项,将它修改为 Universal。这将新增一个 iPad Deployment Info 小节。
在项目中新建一个 Storyboard 文件。选择 New File,选择User Interface 中的 Storyboard 模板,Device Family 设置为 iPad。保存文件为 en.lproj 目录下的MainStoryboard~ipad.storyboard。
打开新建的故事板文件,拖一个 ViewController 到上面。你会看到一个与iPad 屏幕尺寸同样的 View Controller。拖一个 Label 到上面,随便输入一些文字,例如 testing。
返回 Target 设置窗口,在 Summary 的 iPadDeployment Info 小节,选择 MainStoryboard~ipad 作为 Main Storyboard。
然后修改 AppDelegate.m:
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
players =[NSMutableArray arrayWithCapacity:20]; // ...existingcode ...
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
UITabBarController *tabBarController =
(UITabBarController *)self.window.rootViewController;
UINavigationController *navigationController =
[[tabBarController viewControllers] objectAtIndex:0];
PlayersViewController *playersViewController =
[[navigationControllerviewControllers] objectAtIndex:0];
playersViewController.players = players;
GesturesViewController*gesturesViewController = [[tabBarController viewControllers] objectAtIndex:1];
gesturesViewController.players = players;
}
return YES;
}
我们对程序是否运行在 iPad 上进行了判断,但我们还没有做任何特殊的事情,现在iPad 故事板中还没有 Players 和 Gestures 这两个 ViewController。
在 iPad 模拟器上运行程序。现在看见的不再是原来的那个 tab bar窗口了,而是一个包含了 testing 字样的空白窗口。
iPad 版成功地加载了属于它的故事板。
iPad 的故事板除了比较大些外,其它和 iPhone的并没有太多不同。iPad中还有两种 segue 可选:Popover 和 Replace。
删除 iPad 版中的 ViewController,然后拖入一个Split View Controller 到画布中。这个 Split View Controller 又会附带另外 3 个 ViewController……你该换一台大点的显示器!
默认的 Split View Controller 是竖向的,当你将它的 Oritentation(在Simulated Metrics下面)设置为横向,就可以看到 iPad 屏幕被分为左右两栏。
在这些 ViewController 之间的由箭头来表示相互关系。类似于导航控制器和TabBar 控制器,Split View 控制器也是一种 ViewController 容器。在程序运行时,iPhone 一次只能看到一个ViewController,但 iPad 可以同时看到多个 ViewController,例如 Split View 控制器的主/细窗口。
现在运行程序,它还会有些问题。当你翻转屏幕(或者模拟器),仍然只能看到一个不会旋转的空白窗口。在项目中添加新的UIViewController 子类 DetailViewController,用于作为SplitViewController 中位于右侧的较大的窗口。
编辑 DetailViewController.h:
@interface DetailViewController: UIViewController
<UISplitViewControllerDelegate>
@property (nonatomic,strong) IBOutletUIToolbar *toolbar;
@end
DetailViewController 类实现了UISplitViewControllerDelegate 委托协议,这样当屏幕发生旋转时就会通知它。
编辑 DetailViewController.m如下:
#import "DetailViewController.h"
@implementationDetailViewController
{
UIPopoverController*masterPopoverController;
}
@synthesize toolbar;
- (void)viewDidUnload {
[super viewDidUnload];
self.toolbar= nil;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}
#pragmamark - UISplitViewControllerDelegate
- (void)splitViewController: (UISplitViewController *)splitViewController
willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem
forPopoverController:(UIPopoverController *)popoverController
{
barButtonItem.title= @"Master";
NSMutableArray*items = [[self.toolbar items] mutableCopy];
[items insertObject:barButtonItematIndex:0];
[self.toolbar setItems:itemsanimated:YES];
masterPopoverController =popoverController;
}
- (void)splitViewController:(UISplitViewController*)splitController willShowViewController:(UIViewController*)viewController
invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
NSMutableArray*items = [[self.toolbar items] mutableCopy];
[items removeObject:barButtonItem];
[self.toolbar setItems:itemsanimated:YES];
masterPopoverController= nil;
}
@end
这是为了支持 SplitViewController 所需要做的最少工作。
在故事板编辑器中,设置较大的窗口的类为DetailViewController。拖一个 ToolBar到这个窗口上。将ToolBar 上默认的那个 Bar Button Item 标题(即Item)设置为 Menu(后面我们将为它加上一个 popover),在 Menu 前面插入一个 Flexible Space。
将 Toolbar 连接到 DetailViewController 的toolbar 属性。设置 toolbar的 autosizing 属性如下:
默认 toolbar 是“粘”在屏幕底部,但我们想让它任何时候都居于屏幕顶部——不然当屏幕旋转时它的位置就不对了。
接下来,我们必须为“Master”窗口(就是嵌在 NavigationController中的 TableViewController)也创建一个 ViewController,也就是 split-view 左边的窗口。这样做的唯一目的是为了我们可以覆盖shouldAutorotateToInterfaceOrientation 方法并在里面返回 YES。在 iPad 版本中,所有可视的 ViewController都必须开启旋屏否则程序的旋屏就会有问题。
新建 UITableViewController 子类MasterViewController。如果勾选了 Targeted for iPad 选项,则它的shouldAutorotateToInterfaceOrientation 方法将被自动实现。
打开 MainStoryboard~ipad.storyboard,选择名为 Root View Controller,将 class 设置为 MasterViewController。Xcode会显示缺少数据源方法的警告,暂时不用理会它。
还有一点。DetailViewController 类是 SplitView Controller 的委托,但我们还未为它们创建委托连接。很不幸,你无法直接在故事板中创建这类连接。我们不得不在 AppDelegate 中写一些代码。
打开 AppDelegate.m,修改 didFinishLaunchingWithOptions 方法:
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// ...existing code ...
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
// ...existing code...
} else {
UISplitViewController *splitViewController=
(UISplitViewController *)self.window.rootViewController;
splitViewController.delegate = [splitViewController.
viewControllerslastObject];
return YES;
}
}
运行程序,你已经拥有一个完整的 Split View Controller了。
Xcode 自带一个 Master-Detail Application 模板,但我们想让你知道如何从零开始搭建一个主细窗口的应用。
可以用故事板编辑器设置 Master 窗口的大小。在 SpliteView Controller 的属性模板中设置 popover size 即可。
也可以直接创建 popover。在故事板中添加一个新的 ViewController,然后用一个Popover Segue 指向它。
向画布中拖入一个 ViewController。这将用于作为popover 的 contentView。它稍微显得大了点,你可以将它的 size 由 Inferred 改为 Freeform,同时将状态栏占据的空间移除。
现在可以在 Size 模板中修改它的大小。改成 400x400 即可。为了清晰地看见popover,我把它的背景色改成了 Scroll View Textured Background。
用右键从 Menu 按钮拖一条线到新的 ViewController 上并选择Segue 类型为 Popover。命名 segue 为 ShowPopover。注意,对于 Popover Segue,有几个属性是专用于UIPopoverController 。
运行 app,你可以看到 popover 工作正常。很简单,是吧……
负责呈现 popover 的 segue 是 UIStoryboardPopoverSegue,它是UIStoryboardSeque 的子类。它在 segue 的基础上增加了一个 popoverController 属性,用于指向一个UIPopoverController。这样,我们就可以通过 popoverController 属性来解散它。在DetailViewController.h 中声明对 UIPopoverControllerDelegate 协议的实现:
@interfaceDetailViewController : UIViewController<UISplitViewControllerDelegate, UIPopoverControllerDelegate>
在 DetailViewController.m 中加一个新的实例变量:
@implementationDetailViewController
{
UIPopoverController*masterPopoverController;
UIPopoverController *menuPopoverController;
}
接着是你早已熟悉的 prepareForSegue方法:
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"ShowPopover"])
{
menuPopoverController =
((UIStoryboardPopoverSegue *)segue).popoverController;
menuPopoverController.delegate= self;
}
}
我们将 segue 的 popoverController 属性保存在我们自己的menuPopoverController 变量中,然后让 self 作为这个 popoverController 的委托。
实现委托方法:
#pragmamark - UIPopoverControllerDelegate
- (void)popoverControllerDidDismissPopover:
(UIPopoverController *)thePopoverController
{
menuPopoverController.delegate = nil;
menuPopoverController =nil;
}
在 popover 被解散后,简单地将实例变量设置为 nil。
在我们保存了 segue 的 popoverController 属性后,我们可以在屏幕旋转时解散popover:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation) toInterfaceOrientationduration:(NSTimeInterval)duration
{
if (menuPopoverController != nil&&
menuPopoverController.popoverVisible) {
[menuPopoverController dismissPopoverAnimated:YES];
menuPopoverController= nil;
}
}
运行程序,查看效果。
你可能会发现一个小问题:每当你点击 Menu 按钮,一个 popover 会弹出,但之前打开的popover 并没有被关闭。反复点击 Menu 按钮,终将占满整个 popover 栈。这个问题很烦人(可能导致你的 app 被拒绝),但幸运的是:解决的方法倒也简单。
segue 一旦执行就不可能取消它,因此我们不能用取消 segue 的方法。由于popover 弹出时,我们都保存了一个它的引用,因此我们可以在 prepareForSegue 中这样做:
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"ShowPopover"])
{
if (menuPopoverController!= nil &&
menuPopoverController.popoverVisible) {
[menuPopoverControllerdismissPopoverAnimated:NO];
}
menuPopoverController=
((UIStoryboardPopoverSegue *)segue).popoverController;
menuPopoverController.delegate = self;
}
}