2
votes

I'm trying to add a simple push animation between UIViewControllers using a custom segue.

(Without converting my controllers to use a UINavigationController)

The answers found so far work fine with a navigation controller, but fail when not using a navigation controller. (I am still reading and trying other answers I've seen here on stack-overflow)

My custom segue .m thanks to (ZHCustomSegue.m) Zakir on 7/5/12

#import "SFCustomSegue.h"
#import "QuartzCore/QuartzCore.h"

@implementation SFCustomSegue


-(void)perform {

UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
UIViewController *destinationController = (UIViewController*)[self destinationViewController];

CATransition* transition = [CATransition animation];
transition.duration = 4.0;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;

[sourceViewController.view.layer addAnimation:transition forKey:kCATransition];

[sourceViewController presentViewController:[self destinationViewController] animated:NO completion:nil];

}

If I use a navigation controller as per Zakir's original example and replace the last 2 lines with:

    [sourceViewController.navigationController.view.layer addAnimation:transition
                                                            forKey:kCATransition];

[sourceViewController.navigationController pushViewController:destinationController animated:NO];    

It works....

Apple's doc says (notice not a navigation controller, and using a modal segue):

- (void)perform
{
// Add your own animation code here.

[[self sourceViewController] presentModalViewController:[self destinationViewController] animated:NO];
}

By NOT Working, I get no animation. In Zakir's code, using 4.0 seconds it takes 4.0 seconds and I get the animation I want. Of course, if I use a navigation controller a push tranition is the default. In this code, I get no animation and it takes 0 seconds.

Has anyone gotten custom segue animations to work with UIViewControllers?

3

3 Answers

2
votes

Since you are calling presentViewController, the source view controller is sticking around. If you actually want to replace the source view controller with the destination view controller, so that the source gets dealloced, you can do this:

static UIImageView *screenShotOfView(UIView *view)
{
    // Create a snapshot for animation
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [view.layer renderInContext:context];
    UIImageView *screenShot = [[UIImageView alloc] initWithImage:UIGraphicsGetImageFromCurrentImageContext()];
    UIGraphicsEndImageContext();
    return screenShot;
}

- (void)perform
{
    UIViewController *source = (UIViewController *) self.sourceViewController;
    UIViewController *destination = (UIViewController *) self.destinationViewController;

    // Swap the snapshot out for the source view controller
    UIWindow *window = source.view.window;
    UIImageView *screenShot = screenShotOfView(source.view);
    CGRect originalFrame = destination.view.frame;

    BOOL animsEnabled = [UIView areAnimationsEnabled];
    [UIView setAnimationsEnabled:NO];
    {
        window.rootViewController = destination;
        [window addSubview:screenShot];
        [source.view removeFromSuperview];
        CGRect frame = destination.view.frame;
        frame.origin.x += source.view.bounds.size.width;
        destination.view.frame = frame;
    }
    [UIView setAnimationsEnabled:animsEnabled];

    [UIView animateWithDuration:kAnimationDuration
                     animations:^{
                         destination.view.frame = originalFrame;
                         CGRect frame = screenShot.frame;
                         frame.origin.x -= screenShot.bounds.size.width;
                         screenShot.frame = frame;

                     }
                     completion:^(BOOL finished) {
                         [screenShot removeFromSuperview];
                     }];
}
0
votes

It appears that my above code, is "nearly" working, but not quite. The main issue at hand is likely when the final step is called.

My working code looks like this:

#import "SFCustomSegue.h"
#import "QuartzCore/QuartzCore.h"

@implementation SFCustomSegue

static const float delay = 0.5f;

-(void)perform {

    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];

    [sourceViewController.view addSubview:destinationController.view];

    CATransition* transition = [CATransition animation];
    transition.duration = delay;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromRight;

    [sourceViewController.view.layer addAnimation:transition forKey:kCATransition];

    [self performSelector:@selector(performDone:) withObject:destinationController afterDelay:delay];
}

- (void)performDone:(id)viewController{
    UIViewController *destination = (UIViewController*)viewController;
    [destination.view removeFromSuperview];
    [[self sourceViewController] presentViewController:destination animated:NO completion:nil];

}

@end

The key to resolving this was adding the view for the transition (line 13), then removing it (line 28) and adding it back (line 29) using presentViewController:animated:completion:.

I am still "stuck" with using only the regular CATransition transition types, but that's good enough for me for now, since I want to use kCATransitionPush.

0
votes

I have merged the ideas from both answers to get this code (working good): (the answers from above don't count in the orientation of device so they won't work in landscape, for example)

(the variant with CATransitions wasn't appropriate because there was a gap between two views while the one pushed the another)

#import "PushSegue.h"

/*
@interface TransitionDelegate : NSObject
- (id) initWithScreenshot: (UIView*) screenshot;
@end
*/

@implementation PushSegue
- (void) perform
{
    UIViewController* source = (UIViewController*) self.sourceViewController;
    UIViewController* destination = (UIViewController*) self.destinationViewController;

    const BOOL iPad = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad;
    float animationDuration = iPad ? 0.5 : 0.3;

    // Swap the snapshot out for the source view controller
    UIWindow* window = source.view.window;
    UIImageView* screenShot = screenShotOfView(source.view);

    // accord to device orientation

    float rotation;
    //NSString* transitionType;

    CGRect originalFrame = destination.view.frame;
    CGRect destinationFrame = destination.view.frame;

    switch ([UIApplication sharedApplication].statusBarOrientation)
    {
        case UIInterfaceOrientationPortraitUpsideDown:
            rotation = M_PI;
            destinationFrame.origin.x -= source.view.bounds.size.width;
            //transitionType = kCATransitionFromLeft;
            break;

        case UIInterfaceOrientationLandscapeLeft:
            rotation = M_PI + M_PI_2;
            destinationFrame.origin.y -= source.view.bounds.size.width;
            //transitionType = kCATransitionFromBottom;
            break;

        case UIInterfaceOrientationLandscapeRight:
            rotation = M_PI_2;
            destinationFrame.origin.y += source.view.bounds.size.width;
            //transitionType = kCATransitionFromTop;
            break;

        default:
            rotation = 0;
            destinationFrame.origin.x += source.view.bounds.size.width;
            //transitionType = kCATransitionFromRight;
            break;
    }

    screenShot.transform = CGAffineTransformMakeRotation(rotation);

    // reposition after rotation
    CGRect screenshotFrame = screenShot.frame;
    screenshotFrame.origin.x = 0;
    screenshotFrame.origin.y = 0;
    screenShot.frame = screenshotFrame;

    switch ([UIApplication sharedApplication].statusBarOrientation)
    {
        case UIInterfaceOrientationPortraitUpsideDown:
            screenshotFrame.origin.x += screenShot.bounds.size.width;
            break;

        case UIInterfaceOrientationLandscapeLeft:
            screenshotFrame.origin.y += screenShot.bounds.size.width;
            break;

        case UIInterfaceOrientationLandscapeRight:
            screenshotFrame.origin.y -= screenShot.bounds.size.width;
            break;

        default:
            screenshotFrame.origin.x -= screenShot.bounds.size.width;
            break;
    }

    // swap the view with its screenshot
    window.rootViewController = destination;
    [window addSubview:screenShot];
    [source.view removeFromSuperview];

    /*
    CATransition* transition = [CATransition animation];

    transition.duration = animationDuration;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.type = kCATransitionPush;
    transition.subtype = transitionType;
    transition.delegate = [[TransitionDelegate alloc] initWithScreenshot:screenShot];

    [window addSubview:destination.view];

    [screenShot.window.layer addAnimation:transition forKey:nil];
    */

    const BOOL animationsEnabled = [UIView areAnimationsEnabled];
    [UIView setAnimationsEnabled:NO];
    {
        destination.view.frame = destinationFrame;
    }
    [UIView setAnimationsEnabled:animationsEnabled];

    [UIView animateWithDuration:animationDuration
                     animations:^
    {
        destination.view.frame = originalFrame;
        screenShot.frame = screenshotFrame;
    }
                     completion:^(BOOL finished)
    {
        [screenShot removeFromSuperview];
    }];

    /*
    const BOOL animationsEnabled = [UIView areAnimationsEnabled];
    [UIView setAnimationsEnabled:NO];
    {
        CGRect frame = destination.view.frame;
        frame.origin.x += source.view.bounds.size.width;
        destination.view.frame = frame;
    }
    [UIView setAnimationsEnabled:animationsEnabled];

    [UIView animateWithDuration:animationDuration
                     animations:^
    {
        destination.view.frame = originalFrame;
        CGRect frame = screenShot.frame;
        frame.origin.x -= screenShot.bounds.size.width;
        screenShot.frame = frame;
    }
    completion:^(BOOL finished)
    {
        [screenShot removeFromSuperview];
    }];
    */

    /*
    CATransition* transition = [CATransition animation];

    transition.duration = animationDuration;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.type = kCATransitionPush;

    switch ([UIApplication sharedApplication].statusBarOrientation)
    {
        case UIInterfaceOrientationPortraitUpsideDown:
            transition.subtype = kCATransitionFromLeft;
            break;

        case UIInterfaceOrientationLandscapeLeft:
            transition.subtype = kCATransitionFromBottom;
            break;

        case UIInterfaceOrientationLandscapeRight:
            transition.subtype = kCATransitionFromTop;
            break;

        default:
            transition.subtype = kCATransitionFromRight;
            break;
    }

    [source.view.window.layer addAnimation:transition forKey:nil];

    [source presentViewController:destination animated:NO completion:nil];
    */
}

static UIImageView* screenShotOfView(UIView* view)
{
    // Create a snapshot for animation
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    [view.layer renderInContext:context];
    UIImageView* screenShot = [[UIImageView alloc] initWithImage:UIGraphicsGetImageFromCurrentImageContext()];

    UIGraphicsEndImageContext();

    return screenShot;
}
@end

/*
@implementation TransitionDelegate
{
    UIView* screenshot;
}

- (id) initWithScreenshot: (UIView*) screenshot
{
    if (self = [super init])
    {
        self->screenshot = screenshot;
    }
    return self;
}

- (void) animationDidStop: (CAAnimation*) theAnimation
                 finished: (BOOL) flag
{
    [screenshot removeFromSuperview];
}
@end
*/