實現自定義的Container View Controller
上一篇文章中提到了如何定製Segue。我們知道Unwind Segue的正常工作需要Container View Controller的支援。我們可以實現:
canPerformUnwindSegueAction:fromViewController:withSender:
viewControllerForUnwindSegueAction:fromViewController:withSender:
segueForUnwindingToViewController:fromViewController:identifier:
三個方法來定製自己的Container View Controller(以下簡稱“容器”)。
我們一般會在子Controller中通過實現canPerformUnwindSegueAction:fromViewController:withSender:
來決定要不要執行相應的Unwind Segue。
在自定義的容器中,我們必須實現viewControllerForUnwindSegueAction:fromViewController:withSender:
和segueForUnwindingToViewController:fromViewController:identifier:
方法。前一個方法用來決定那個View Controller來處理Unwind Segue action,後一個方法用來返回自定義的Unwind Segue例項。
使用Modal presentation時需要注意的情況
當我們使用UIViewController
的presentViewController:animated:completion:
方法以Modal presentation的方式來跳轉場景的時候,情況與在Navigation View Controller有很大不同。首先,使用這種方式跳轉場景的時候,跳轉到的View Controller為Source View Controller的子Controller,而在Navigation View Controller中,所有的流程Controller基本上都是Navgation View Controller的子Controller,所以二者在View Controller的層次管理上有很多不同。因此實現Modal presentation風格的Segue的時候,動畫的view不能搞錯,必須對View Controller中的頂層View操作。一個參考實現如下(略掉動畫效果程式碼,僅提供轉場方法呼叫程式碼)1:
Segue部分:
- (UIView *)findTopMostViewForViewController:(UIViewController *)viewController
{
UIView *theView = viewController.view;
UIViewController *parentViewController = viewController.parentViewController;
while (parentViewController != nil)
{
theView = parentViewController.view;
parentViewController = parentViewController.parentViewController;
}
return theView;
}
- (void)perform
{
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
// Find the views that we will be animating. If the source or destination
// view controller sits inside a container view controller, then the view
// to animate will actually be that parent controller`s view.
UIView *sourceView = [self findTopMostViewForViewController:source];
UIView *destinationView = [self findTopMostViewForViewController:destination];
[source presentViewController:destination animated:NO completion:^{
// completion code here
}];
}
Unwind Segue部分:
- (UIView *)findTopMostViewForViewController:(UIViewController *)viewController
{
UIView *theView = viewController.view;
UIViewController *parentViewController = viewController.parentViewController;
while (parentViewController != nil)
{
theView = parentViewController.view;
parentViewController = parentViewController.parentViewController;
}
return theView;
}
- (void)perform
{
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
// Find the views that we will be animating. If the source or destination
// view controller sits inside a container view controller, then the view
// to animate will actually be that parent controller`s view.
UIView *sourceView = [self findTopMostViewForViewController:source];
UIView *destinationView = [self findTopMostViewForViewController:destination];
[source dismissViewControllerAnimated:NO completion:^{
// completion code here
}];
}
注意:Modal Presentation的Unwind Segue很難實現無Bug的任意跳轉,因為UIViewController
中,跟Container View Controller相關的方法的預設實現並不能很好的定位Container View Controller。而以正確的方式重寫這些方法並不容易。所以如果有任意跳轉的需求,我們可以嘗試自己實現一個簡單的Container View Controller。
使用AddChildViewController API實現自己的Container View Controller
我們偶爾會希望有一個跟Navigation View Controller差不多的容器,但是又不希望像Navigation View Controller那麼笨重,且限制多多。我們知道Navigation View Controller在Interface Builder中,其Navigation Bar能容納的元素樣式並不豐富,儘管大多數時候,我們能夠通過UIAppearance來定製一些樣式,但我們希望定製能容納更加豐富的元素的Navigation Bar,或者其他定製的導航介面的時候,希望能夠實現一個類似的容器。我們當然可以模仿Navigation View Controller的公開API實現一個差不多的東西,如果我們要很方便的使用自定義Segue和任意跳轉的Unwind Segue的話,還需要以特定的方式實現上面提到的一些方法。UIViewController
的addChildViewController:
方法同樣可以做出類似的功能,而且相比Modal presentation,這種方式程式碼更加直觀。因為使用這個API實現的容器,對子Controller的管理方式與Navigation View Controller類似。
容器的部分程式碼如下:
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
for (UIViewController *childController in self.childViewControllers) {
if ([childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender]) {
return childController;
}
}
return nil;
}
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier
{
UIStoryboardSegue *unwindSegue = [[MyLeftToRightUnwindSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
return unwindSegue;
}
Segue程式碼:
- (BOOL)controllerInStack:(UIViewController *)controller
{
UIViewController *fromController = self.sourceViewController;
UIViewController *containerController = fromController.parentViewController;
for (UIViewController *childController in containerController.childViewControllers) {
if (childController == controller) {
return YES;
}
}
return NO;
}
- (void)perform
{
// A simple transition.
// New scene slides in from right and old scene slides out to left.
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIViewController *parentController = fromController.parentViewController;
UIView *containerView = parentController.view;
[containerView addSubview:toController.view];
CGRect initialFromRect = fromController.view.frame;
CGRect initialToRect = CGRectOffset(initialFromRect, initialFromRect.size.width, 0);
CGRect finalFromRect = CGRectOffset(initialFromRect, -initialFromRect.size.width, 0);
CGRect finalToRect = initialFromRect;
toController.view.frame = initialToRect;
if (![self controllerInStack:toController]) {
// notify containment event.
[toController willMoveToParentViewController:parentController];
}
[UIView animateWithDuration:0.4f animations:^{
fromController.view.frame = finalFromRect;
toController.view.frame = finalToRect;
} completion:^(BOOL finished) {
if (![self controllerInStack:toController]) {
// Add new controller as a child controller to the container view controller
[parentController addChildViewController:toController];
// notify containment event.
[toController didMoveToParentViewController:toController];
}
[fromController.view removeFromSuperview];
}];
}
Unwind Segue程式碼:
- (void)perform
{
// A simple transition.
// New scene slides in from left and old scene slides out to right.
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIViewController *parentController = fromController.parentViewController;
UIView *containerView = parentController.view;
[containerView addSubview:toController.view];
CGRect initialFromRect = fromController.view.frame;
CGRect initialToRect = CGRectOffset(initialFromRect, -initialFromRect.size.width, 0);
CGRect finalFromRect = CGRectOffset(initialFromRect, initialFromRect.size.width, 0);
CGRect finalToRect = initialFromRect;
toController.view.frame = initialToRect;
[UIView animateWithDuration:0.4f animations:^{
fromController.view.frame = finalFromRect;
toController.view.frame = finalToRect;
} completion:^(BOOL finished) {
[fromController.view removeFromSuperview];
}];
}
當我們定義的Container View中有需要置頂的元素(比如定製的導航條)時,可以將addSubView:
方法換成insertSubView:atIndex:
方法來調整子檢視的層次。
-
下面的程式碼修改自iOS6 by Tutorial中的示例程式碼。 ↩