现在,不管你点击 Menu 按钮多少次都没事了,任何时候只会有一个popover 显示。
除了 Popover segue,iPad 故事板还有 Replacesegue。用它,你可以替换 Split View Controller 中的 master 或 detail 窗口。以设置程序为例,master(左边)是一个Table View,它的每一行由是一个 detail view。你可以用 Replace segue 将每一行和它的 detail view 关联起来,每当你点击table view 中的一行,显示一个 detail 窗口。
segue 也可以以 UIModalPresentationFormSheet或者 UIModalPresentationPageSheet 风格呈现模式窗体,只需要设置目标 ViewController 的Presentation 属性即可:
译者注:
UIModalPresentationFullScreen代表弹出VC时,presentedVC充满全屏,如果弹出VC的wantsFullScreenLayout设置为YES的,则会填充到状态栏下边,否则不会填充到状态栏之下。
UIModalPresentationPageSheet代表弹出是弹出VC时,presentedVC的高度和当前屏幕高度相同,宽度和竖屏模式下屏幕宽度相同,剩余未覆盖区域将会变暗并阻止用户点击,这种弹出模式下,竖屏时跟UIModalPresentationFullScreen的效果一样,横屏时候两边则会留下变暗的区域。
UIModalPresentationFormSheet这种模式下,presentedVC的高度和宽度均会小于屏幕尺寸,presented VC居中显示,四周留下变暗区域。
UIModalPresentationCurrentContext这种模式下,presentedVC的弹出方式和presenting VC的父VC的方式相同。
这四种方式在iPad上面统统有效,但在iPhone和iPod touch上面系统始终已UIModalPresentationFullScreen模式显示presentedVC。
手动加载故事板
iPad 版还没有打分功能,我们可以将 iPhone 故事板中的 ViewController 添加到 Split-View 的 master 视图。从一个故事板中加载另一个故事板只能以编码的方式进行。
注意:我不建议你的在其他的 universal 应用中这样干。对于 universal app,应该始终让 iPhone 和iPad 的故事板保持分开。这里只是为了演示如何手动加载故事板,因为有时候你不得不在一个app 中加载多个故事板。
从 iPad 故事板中删除导航控制器和 Root ViewController。也可以删除 MasterViewController 类。 当前的故事板编辑器如下图所示:
我们会在 App Delegate 中加载 iPhone 故事板,把它的TabBarController 放到 split-view 的 Master 窗口中去。UIStoryboard 类负责呈现故事板。主故事板文件在应用程序启动时自动加载,如果你要加载额外的故事板,得调用
[UIStoryboard storyboardWithName:bundle:] 方法。
如果你想加载 iPhone 故事板,首先就得使用代码:
UIStoryboard*storyboard =
[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
由于我们的 app 是在 iPad 中运行的,它会去加载MainStroyboard~ipad 文件,这不是我们所期望的。
一般情况下你不会用代码去加载故事板,所以也没什么问题。但在这里,我们最好是重命名 MainStoryboard~ipad 文件。把它命名为“iPadMainStoryboard.storyboard”。
别忘了也要在 target 的 Summary 窗口中修改 iPadDeployment Info 小节中的 Main Storyboard 选项。最好 clean 一下以删除模拟器中的 app,确保清除缓存的故事板。
在 AppDelegate.m中修改 didFinishLaunchingWithOptions 方法:
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// ...existing code ...
UITabBarController *tabBarController;
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad)
{
tabBarController= (
UITabBarController*)self.window.rootViewController;
}else{
UISplitViewController *splitViewController=
(UISplitViewController *)self.window.rootViewController;
UIStoryboard*storyboard = [UIStoryboard storyboardWithName: @"MainStoryboard"bundle:nil];
tabBarController= [storyboard instantiateInitialViewController];
NSArray *viewControllers= [NSArray arrayWithObjects:tabBarController,
[splitViewController.viewControllers lastObject], nil];
splitViewController.viewControllers = viewControllers;
splitViewController.delegate = [splitViewController.viewControllers lastObject];
}
UINavigationController *navigationController= [[tabBarController viewControllers] objectAtIndex:0];
PlayersViewController *playersViewController= [[navigationController viewControllers] objectAtIndex:0];
playersViewController.players= players;
GesturesViewController *gesturesViewController= [[tabBarController viewControllers] objectAtIndex:1];
gesturesViewController.players= players;
return YES;
}
看这个地方:
UIStoryboard*storyboard = [UIStoryboard storyboardWithName: @"MainStoryboard"bundle:nil];
tabBarController =[storyboard instantiateInitialViewController];
这会加载 MainStoryboard 文件(iPhone 故事板)到新的UIStoryboard 对象中。然后通过 instantiateInitialViewController 方法加载它的第一个 ViewController,即我们的TabBarController。
拿到 TabBarController 之后,我们需要把它放到split-view 控制器中。当前 split-view 中只包含有 detail 视图,我们需要把 TabBarController 加到split-view 控制器的 viewControllers 数组中去:
NSArray*viewControllers = [NSArray arrayWithObjects: tabBarController,
[splitViewController.viewControllers lastObject], nil];
splitViewController.viewControllers = viewControllers;
运行程序,你可以看到在 split-view 的 popover 中显示了 iPhone 故事板中的内容:
如果你点击 Players 窗口中的内容,你会发现这种集成并不是十分的完美。因为在iPhone 的故事板中我们本来就没有为适应 iPad 屏幕而做过任何的配置。
在所有 ViewController 的 .m 文件中修改 shouldAutorotateToInterfaceOrientation方法为:
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
return YES;
return (interfaceOrientation!=
UIInterfaceOrientationPortraitUpsideDown);
}
现在,程序会正确地适应于横屏。但是,在 iPhone 故事板中,故事板编辑器不允许设置popover 窗口的大小(因为 iPhone 下没有 popover 控制器),因此我们得在代码中设置。在application:didFinish LaunchingWithOptions 方法中加入代码:
tabBarController.contentSizeForViewInPopover = CGSizeMake(320,460);
现在,split-view 弹出的 popover 正好与 iPhone 故事板中的ViewController 大小一致。
现在我们的程序有一个小问题,模式窗体会以全屏方式弹出,看起来有点怪异。这和 ViewController 的 modal presentation style 属性有关。如果在 prepareForSegue 方法中你使用了
navigationController.modalPresentationStyle = UIModalPresentationCurrentContext;
则模式窗体就不会占据全屏了——它仅会填充 master 视图。
UIStoryboard 的 instantiateInitialViewController 方法并不是唯一的从故事板中加载ViewController 的方法。你可以用instantiateViewControllerWithIdentifier方法加载指定的 ViewController。如果在故事板中该 ViewController 并没有任何 segue,这个方法就很有用了。
让我们来演示一下。打开 iPhone 故事板,从 Players 中删除 EditPlayersegue。这个 segue 是被 disclosure 按钮所触发的。(提示:如果你现在运行程序,点击 disclosure 按钮,程序会崩溃并报告如下错误: "Receiver (<PlayersViewController>) has no seguewith identifier 'EditPlayer'")。
将 PlayersViewController.m 中的accessoryButtonTappedForRowWithIndexPath 方法修改为:
- (void)tableView:(UITableView*)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
UINavigationController*navigationController =
[self.storyboard instantiateViewControllerWithIdentifier: @"PlayerDetails"];
PlayerDetailsViewController *playerDetailsViewController= [[navigationController viewControllers] objectAtIndex:0];
playerDetailsViewController.delegate = self;
Player *player= [self.players objectAtIndex:indexPath.row];
playerDetailsViewController.playerToEdit = player;
[self presentViewController:navigationController animated:YES completion:nil];
}
这很像是我们曾经在 prepareForSegue 方法中所做的,除了这行:
UINavigationController*navigationController = [self.storyboard instantiateViewControllerWithIdentifier:
@"PlayerDetailsNavigationController"];
self.storyboard 属性引用了实例化该 ViewController 时所加载的 UIStoryboard 对象。我们用这个UIStoryboard 对象来实例化指定的 ViewController。这里即包含有玩家详情窗口的导航控制器。
要想让这行代码能正确工作,你必须在属性面板中设置该 ViewController的 identifier 属性。
运行程序(在模拟器中),编辑一个玩家。你会看到程序在没有 EditPlayersegue 的情况下仍然正常工作。
注意:我们要初始化的是导航控制器,因此设置 identifier 的时候设置的是导航控制器的 identifier 属性,而不是 PlayerDetailsViewController的 identifier 属性。
结束语
要本地化故事板是很容易的,如同本地化其他资源一样。选中故事板,在文件面板的Location 栏中增加新的语言,点击 Done。
现在,故事板中的每个 ViewController 都有一个列出顶层对象的dock 栏,一般都会包括 First Responder 以及该 ViewController。Gesture recognizer也会放在 dock上。理论上,你可以像在nib文件中一样,将任何对象拖到dock 上,但问题在于故事板编辑器无法编辑这些对象。
如果你想修改一个静态cell 的背景图片,你可以拖一个 UIImageView到 dock 上,然后将它连接到 cell 的 backgroundImage 属性。但你仅能通过面板编辑这个 ImageView,它不会被加到画布中。我希望故事板开发者能添加这个功能,因为这对于加载额外的对象很方便。
另外,你可以向故事板中放入标准的 ViewController 比如UIImagePickerController 和 MFMailComposeViewController。拖一个普通的ViewController ,然后将它的class 设置为 UIImagerPickerController 即可。在 prepareForSegue 方法里,设置 ViewController 的属性即可。我不太清楚故事板是否一直支持这种做法,但目前为止我还没有发现有什么问题;-)