- Developing with Navigation Controllers and Split Views
- Recipe: Building a Simple Two-Item Menu
- Recipe: Adding a Segmented Control
- Recipe: Navigating Between View Controllers
- Recipe: Presenting a Custom Modal Information View
- Recipe: Page View Controllers
- Recipe: Scrubbing Pages in a Page View Controller
- Recipe: Tab Bars
- Recipe: Remembering Tab State
- Recipe: Building Split View Controllers
- Recipe: Creating Universal Split View/Navigation Apps
- Recipe: Custom Containers and Segues
- One More Thing: Interface Builder and Tab Bar Controllers
- Summary
Recipe: Custom Containers and Segues
Apple's split view controller was groundbreaking in that it introduced the notion that more than one controller could live onscreen at a time. Until the split view, the rule was one controller with many views at a time. With split view, several controllers co-existed onscreen, all of them independently responding to orientation and memory events.
Apple exposed this multiple-controller paradigm to developers in the iOS 5 SDK. You can now create a parent controller and add child controllers to it. Events are passed from parent to child as needed. This allows you to build custom containers, outside of the Apple-standard set of containers such as tab bar and navigation controllers. Here is how you might load children from a storyboard and add them to a custom array of child view controllers:
UIStoryboard *aStoryboard = [UIStoryboard storyboardWithName:@"child" bundle:[NSBundle mainBundle]]; childControllers = [NSArray arrayWithObjects: [aStoryboard instantiateViewControllerWithIdentifier:@"0"], [aStoryboard instantiateViewControllerWithIdentifier:@"1"], [aStoryboard instantiateViewControllerWithIdentifier:@"2"], [aStoryboard instantiateViewControllerWithIdentifier:@"3"], nil]; // Set each child as a child view controller, setting its frame for (UIViewController *controller in childControllers) { controller.view.frame = backsplash.bounds; [self addChildViewController:controller]; }
With custom containers comes their little brother, custom segues. Just as tab and navigation controllers provide a distinct way of transitioning between child controllers, you can build custom segues that define animations unique to your class. There's not a lot of support in Interface Builder for custom containers with custom segues, so it's best to develop your presentations in code at this time. Here's how you might implement the code that moves the controller to a new view:
// Informal delegate method - (void) segueDidComplete { pageControl.currentPage = vcIndex; } // Transition to new view using custom segue - (void) switchToView: (int) newIndex goingForward: (BOOL) goesForward { if (vcIndex == newIndex) return; // Segue to the new controller UIViewController *source = [childControllers objectAtIndex:vcIndex]; UIViewController *destination = [childControllers objectAtIndex:newIndex]; RotatingSegue *segue = [[RotatingSegue alloc] initWithIdentifier:@"segue" source:source destination:destination]; segue.goesForward = goesForward; segue.delegate = self; [segue perform]; vcIndex = newIndex; }
Here, the code identifies the source and destination child controllers, builds a segue, sets its parameters, and tells it to perform. An informal delegate method is called back by that custom segue on its completion. Recipe 5-11 shows how that segue is built. In this example, it creates a rotating cube effect that moves from one view to the next. Figure 5-8 shows the segue in action.
Figure 5-8 Custom segues allow you to create visual metaphors for your custom containers. Recipe 5-11 builds a "cube" of view controllers that can be rotated from one to the next.
The segue's goesForward property determines whether the rotation moves to the right or left around the virtual cube. Although this example uses four view controllers, as you saw in the code that laid out the child view controllers, that's a limitation of the metaphor, not of the code itself, which will work with any number of child controllers. You can just as easily build three- or seven-sided presentations with this, although you are breaking an implicit "reality" contract with your user if you do so. To add more (or fewer) sides, you should adjust the animation geometry in the segue away from a cube to fit your virtual n-hedron.
Recipe 5-11. Creating a Custom View Controller Segue
@implementation RotatingSegue @synthesize goesForward; @synthesize delegate; // Return a shot of the given view - (UIImage *)screenShot: (UIView *) aView { // Arbitrarily dims to 40%. Adjust as desired. UIGraphicsBeginImageContext(hostView.frame.size); [aView.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); CGContextSetRGBFillColor(UIGraphicsGetCurrentContext(), 0, 0, 0, 0.4f); CGContextFillRect (UIGraphicsGetCurrentContext(), hostView.frame); UIGraphicsEndImageContext(); return image; } // Return a layer with the view contents - (CALayer *) createLayerFromView: (UIView *) aView transform: (CATransform3D) transform { CALayer *imageLayer = [CALayer layer]; imageLayer.anchorPoint = CGPointMake(1.0f, 1.0f); imageLayer.frame = (CGRect){.size = hostView.frame.size}; imageLayer.transform = transform; UIImage *shot = [self screenShot:aView]; imageLayer.contents = (__bridge id) shot.CGImage; return imageLayer; } // On starting the animation, remove the source view - (void)animationDidStart:(CAAnimation *)animation { UIViewController *source = (UIViewController *) super.sourceViewController; [source.view removeFromSuperview]; } // On completing the animation, add the destination view, // remove the animation, and ping the delegate - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished { UIViewController *dest = (UIViewController *) super.destinationViewController; [hostView addSubview:dest.view]; [transformationLayer removeFromSuperlayer]; if (delegate) SAFE_PERFORM_WITH_ARG(delegate, @selector(segueDidComplete), nil); } // Perform the animation -(void)animateWithDuration: (CGFloat) aDuration { CAAnimationGroup *group = [CAAnimationGroup animation]; group.delegate = self; group.duration = aDuration; CGFloat halfWidth = hostView.frame.size.width / 2.0f; float multiplier = goesForward ? -1.0f : 1.0f; // Set the x, y, and z animations CABasicAnimation *translationX = [CABasicAnimation animationWithKeyPath:@"sublayerTransform.translation.x"]; translationX.toValue = [NSNumber numberWithFloat:multiplier * halfWidth]; CABasicAnimation *translationZ = [CABasicAnimation animationWithKeyPath:@"sublayerTransform.translation.z"]; translationZ.toValue = [NSNumber numberWithFloat:-halfWidth]; CABasicAnimation *rotationY = [CABasicAnimation animationWithKeyPath:@"sublayerTransform.rotation.y"]; rotationY.toValue = [NSNumber numberWithFloat: multiplier * M_PI_2]; // Set the animation group group.animations = [NSArray arrayWithObjects: rotationY, translationX, translationZ, nil]; group.fillMode = kCAFillModeForwards; group.removedOnCompletion = NO; // Perform the animation [CATransaction flush]; [transformationLayer addAnimation:group forKey:kAnimationKey]; } - (void) constructRotationLayer { UIViewController *source = (UIViewController *) super.sourceViewController; UIViewController *dest = (UIViewController *) super.destinationViewController; hostView = source.view.superview; // Build a new layer for the transformation transformationLayer = [CALayer layer]; transformationLayer.frame = hostView.bounds; transformationLayer.anchorPoint = CGPointMake(0.5f, 0.5f); CATransform3D sublayerTransform = CATransform3DIdentity; sublayerTransform.m34 = 1.0 / -1000; [transformationLayer setSublayerTransform:sublayerTransform]; [hostView.layer addSublayer:transformationLayer]; // Add the source view, which is in front CATransform3D transform = CATransform3DMakeIdentity; [transformationLayer addSublayer: [self createLayerFromView:source.view transform:transform]]; // Prepare the destination view either to the right or left // at a 90/270 degree angle off the main transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); transform = CATransform3DTranslate(transform, hostView.frame.size.width, 0, 0); if (!goesForward) { transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); transform = CATransform3DTranslate(transform, hostView.frame.size.width, 0, 0); transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); transform = CATransform3DTranslate(transform, hostView.frame.size.width, 0, 0); } [transformationLayer addSublayer: [self createLayerFromView:dest.view transform:transform]]; } // Standard UIStoryboardSegue perform - (void)perform { [self constructRotationLayer]; [self animateWithDuration:0.5f]; } @end
Transitioning Between View Controllers
UIKit offers a simple way to animate view features when you move from one child view controller to another. You provide a source view controller, a destination, and a duration for the animated transition. You can specify the kind of transition in the options. Supported transitions include page curls, dissolves, and flips. This method creates a simple curl from one view controller to the next:
- (void) action: (id) sender { [self transitionFromViewController:redController toViewController:blueController duration:1.0f options:UIViewAnimationOptionLayoutSubviews | UIViewAnimationOptionTransitionCurlUp animations:^(void){} completion:^(BOOL finished){ [redController.view removeFromSuperview]; [self.view addSubview:blueController.view];} ]; }
You can use the same approach to animate UIView properties without the built-in transitions. For example, this method re-centers and fades out the red controller while fading in the blue. These are all animatable UIView features and are changed in the animations: block.
- (void) action: (id) sender { blueController.view.alpha = 0.0f; [self transitionFromViewController:redController toViewController:blueController duration:2.0f options:UIViewAnimationOptionLayoutSubviews animations:^(void){ redController.view.center = CGPointMake(0.0f, 0.0f); redController.view.alpha = 0.0f; blueController.view.alpha = 1.0f;} completion:^(BOOL finished){ [redController.view removeFromSuperview]; [self.view addSubview:blueController.view];} ]; }
Using transitions and view animations is an either/or scenario. Either set a transition option or change view features in the animations block. Otherwise, they conflict, as you can easily confirm for yourself.
Use the completion block to remove the old view and move the new view into place. You should not have to explicitly call didMoveToParentViewController: or any of the related, contained view controller methods.
Although simple to implement, this kind of transition is not meant for use with Core Animation. If you wish to add Core Animation effects to your view-controller-to-view-controller transitions, look at using a custom segue instead.