Home > Articles > Home & Office Computing > Entertainment/Gaming/Gadgets

  • Print
  • + Share This
This chapter is from the book

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 on-screen 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 on-screen, 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 like 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 identifiers 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 8 (Click to Enlarge) 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 3- or 7-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 and 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 recenters 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.

While 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.

  • + Share This
  • 🔖 Save To Your Account