Implement custom animation to present modal view from specified view on iPad

18,755

Solution 1

What I did was creating a new category for UIViewController as follows

UIViewController+ShowModalFromView.h

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view;

@end

UIViewController+ShowModalFromView.m

#import "UIViewController+ShowModalFromView.h"

@implementation UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view
{
    modalViewController.modalPresentationStyle = UIModalPresentationFormSheet;

    // Add the modal viewController but don't animate it. We will handle the animation manually
    [self presentModalViewController:modalViewController animated:NO];

    // Remove the shadow. It causes weird artifacts while animating the view.
    CGColorRef originalShadowColor = modalViewController.view.superview.layer.shadowColor;
    modalViewController.view.superview.layer.shadowColor = [[UIColor clearColor] CGColor];

    // Save the original size of the viewController's view    
    CGRect originalFrame = modalViewController.view.superview.frame;

    // Set the frame to the one of the view we want to animate from
    modalViewController.view.superview.frame = view.frame;

    // Begin animation
    [UIView animateWithDuration:1.0f
                     animations:^{
                         // Set the original frame back
                         modalViewController.view.superview.frame = originalFrame;
                     }
                     completion:^(BOOL finished) {
                         // Set the original shadow color back after the animation has finished
                         modalViewController.view.superview.layer.shadowColor = originalShadowColor;
                     }];
}

@end

It's pretty straight forward. Please let me know if this helps you.

UPDATE

I've updated the answer to use animation blocks instead of [UIView beginAnimations:nil context:nil]; / [UIView commitAnimations] pair.

Solution 2

Seems like you're essentially after translating (moving) a CALayer while scaling it down and rotating it about the y-axis at the same time. Try this:

NSValue *initialTransformValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
CATransform3D translation = CATransform3DMakeTranslation(finalPoint.x, finalPoint.y, 0.0);
CATransform3D scalingAndTranslation = CATransform3DScale(translation, kMyScalingFactor, kMyScalingFactor, 1.0);
CATransform3D finalTransform = CATransform3DRotate(scalingAndTranslation, myRotationAngle, 0.0, 1.0, 0.0);
NSArray *keyFrameValues = [NSArray arrayWithObjects:initialTransformValue, [NSValue valueWithCATransform3D:finalTransform], nil];
CAKeyframeAnimation *myAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
myAnimation.values = keyFrameValues;
myAnimation.duration = kMyAnimationDuration;
myAnimation.delegate = self;
myAnimation.removedOnCompletion = NO;
myAnimation.fillMode = kCAFillModeForwards;
[myLayer addAnimation:myAnimation forKey:@"myAnimationKey"];
  • finalPoint should be a CGPoint in the coordinate space of myLayer.
  • kMyScalingFactor should be <1.0 for scaling down and >1.0 for scaling up.
  • myRotationAngle should be in radians. Use positive values for rotating clockwise and negative values for counter-clockwise.

You also need to implement an animation termination handler to make the animation "stick":

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    myLayer.transform = finalTransform;
    myLayer removeAnimationForKey:@"myAnimationKey"];
}

Hope this helps.

Solution 3

Mihai's answer does not handle flipping as iPad itunes animation does. I changed it a little bit to flip the view. You don't need all kinds of crazy CAAnimation stuff, just a few built-in UIView animation functions.

#import <QuartzCore/QuartzCore.h>
@interface UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view;

@end
@implementation UIViewController (ShowModalFromView)
- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view
{
    NSTimeInterval scaleSpeed = 0.3;
    NSTimeInterval flipSpeed = 0.4;

    UIView __weak *containerView = view.superview;

    view.autoresizesSubviews = YES;

    [self presentModalViewController:modalViewController animated:NO];

    UIView __weak *presentedView = modalViewController.view.superview;

    //intead of show the actual view of modalViewController, we are showing the snapshot of it to avoid layout problem
    UIGraphicsBeginImageContext(presentedView.bounds.size);
    [presentedView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage* modalSnapshot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    UIView __weak *originalSuperView = presentedView.superview;   


    CGRect originalFrame = presentedView.frame;
    CGRect frameInContainer = [containerView convertRect:originalFrame fromView:originalSuperView];
    [presentedView removeFromSuperview];
    UIImageView* snapshotView = [[UIImageView alloc] initWithImage:modalSnapshot];
    snapshotView.autoresizingMask = UIViewAutoresizingNone;


    [containerView bringSubviewToFront:view];

    [UIView animateWithDuration:scaleSpeed delay:0 options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionBeginFromCurrentState animations: ^{
        [UIView animateWithDuration:scaleSpeed
                              delay:0
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations: ^{
                             view.frame = frameInContainer;

                         }

                         completion:nil
         ];


    } completion:^(BOOL finished) {

        [UIView setAnimationBeginsFromCurrentState:YES];
        [UIView transitionWithView:view duration:flipSpeed options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionTransitionFlipFromRight animations:
         ^{
             snapshotView.frame = view.bounds;
             [view addSubview:snapshotView];

         } completion:^(BOOL finished) {
             [originalSuperView addSubview:presentedView];
             [snapshotView removeFromSuperview];
         }
         ];
    }];
}

Solution 4

I've gotten this working before by just animating views.

1) Album artwork is in a grid.
2) Transition the view of the album artwork using the flip animation.
3) Animate the view moving across the screen.

I quickly threw this together. Assuming you have an empty view controller and 3 views.

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self performSelector:@selector(transition) withObject:nil afterDelay:1];
    albumArtworkSquare = [[UIView alloc] initWithFrame:CGRectMake(400, 500, 300, 300)];
    albumArtworkSquare.backgroundColor = [UIColor blackColor];
    [self.view addSubview:albumArtworkSquare];

    frontViewOfAlbumArtwork = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    frontViewOfAlbumArtwork.backgroundColor = [UIColor blueColor];
    [albumArtworkSquare addSubview:frontViewOfAlbumArtwork];

    backViewToTransitionTo = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    backViewToTransitionTo.backgroundColor = [UIColor grayColor];
}

- (void)transition
{

    [UIView animateWithDuration:2 animations:^{
        albumArtworkSquare.frame = CGRectMake(10, 500, 300, 300);
    }];


    [UIView transitionFromView:frontViewOfAlbumArtwork toView:backViewToTransitionTo duration:2 options:UIViewAnimationOptionTransitionFlipFromRight completion:^(BOOL finished)
     {

         [frontViewOfAlbumArtwork removeFromSuperview];
         [albumArtworkSquare addSubview:backViewToTransitionTo];

     }];

}

Solution 5

I am doing something quite similar in one of my projects having a grid of album arts. This is the approach I am taking. The key is to use CAAnimationGroup.

1) The whole animation would include scaling, rotating and moving along a path all at the same time - first for the album art layer and then for the modal view layer.

2)Animate the album art layer by flipping it by 90 degrees, scaling a little bit and moving to a predestined location from its current location. At this point it will disappear(vertical to the screen).

3) Add the modal view. Scale and transform the modal view to be in the exact location that the album art is positioned in step 1.

4) Animate the modal view from this position by scaling, rotating and moving along a path to fill the screen.

5) Remove the modal view.

6) Present modal view with no animation.

7) The path chosen would normally add the center of the screen as a control point. But then that can be changed based on how you want the animation to appear.

Below is a function where you can see the use of animation group. Hope this helps you. I still haven't figured out how to avoid the clipping of the animation by the navigation bars and the tab bars though. :)

+ (void)animateWithCurrentView:(UIView *)currentView 
{
    #define kResizeKey @"bounds.size"
    #define kPathMovement @"position"
    #define kRotation @"transform"
    #define kGroupAnimation @"subviewBeingAnimated"
    #define kLayerAnimation @"animateLayer"

    //flip the view by 180 degrees in its place first.
    currentView.layer.transform = CATransform3DRotate(currentView.layer.transform,radians(180), 0, 1, 0);

    //set the anchor point so that the view rotates on one of its sides.
    currentView.layer.anchorPoint = CGPointMake(0.0, 0.5);



    /**
     * Set up scaling
     */
    CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:kResizeKey];

    //we are going to fill the screen here. So 320,480
    [resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(320, 480)]];
    resizeAnimation.fillMode            = kCAFillModeForwards;
    resizeAnimation.removedOnCompletion = NO;


    /**
     * Set up path movement
     */
    UIBezierPath *movePath = [UIBezierPath bezierPath];

    //the control point is now set to centre of the filled screen. Change this to make the path different.
    CGPoint ctlPoint       = CGPointMake(160.0, 240.0);

    //This is the starting point of the animation. This should ideally be a function of the frame of the view to be animated. Hardcoded here.
    [movePath moveToPoint:CGPointMake(320, 60)];

    //The anchor point is going to end up here at the end of the animation.
    [movePath addQuadCurveToPoint:CGPointMake(0, 240) controlPoint:ctlPoint];

    CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:kPathMovement];

    moveAnim.path                = movePath.CGPath;
    moveAnim.removedOnCompletion = YES;

    /**
     * Setup rotation animation
     */
    CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:kRotation];
    //start from 180 degrees (done in 1st line)
    CATransform3D fromTransform       = CATransform3DMakeRotation(radians(180), 0, 1, 0);
    //come back to 0 degrees
    CATransform3D toTransform         = CATransform3DMakeRotation(radians(0), 0, 1, 0);

    //This is done to get some perspective.
    CATransform3D persp1 = CATransform3DIdentity;
    persp1.m34 = 1.0 / -3000;

    fromTransform = CATransform3DConcat(fromTransform, persp1);
    toTransform = CATransform3DConcat(toTransform,persp1);

    rotateAnimation.toValue             = [NSValue valueWithCATransform3D:toTransform];
    rotateAnimation.fromValue           = [NSValue valueWithCATransform3D:fromTransform];
    //rotateAnimation.duration            = 2;
    rotateAnimation.fillMode            = kCAFillModeForwards;
    rotateAnimation.removedOnCompletion = NO;

    /**
     * Setup and add all animations to the group
     */
    CAAnimationGroup *group = [CAAnimationGroup animation]; 

    [group setAnimations:[NSArray arrayWithObjects:moveAnim,rotateAnimation, resizeAnimation, nil]];

    group.fillMode            = kCAFillModeForwards;
    group.removedOnCompletion = NO;
    group.duration            = 0.7f;
    group.delegate            = self;

    [group setValue:currentView forKey:kGroupAnimation];

    /**
     * ...and go
     */
    [currentView.layer addAnimation:group forKey:kLayerAnimation];

}
Share:
18,755

Related videos on Youtube

Zebs
Author by

Zebs

Entrepreneur, developer. StackOverflow has helped me become a much better developer, I love asking questions and I try to answer as many as I can; trying to give back as much as I can to this amazing community that has helped me so much.

Updated on June 01, 2022

Comments

  • Zebs
    Zebs almost 2 years

    On the iPad we get much more room to work with, so presenting full screen modal views is not ideal.

    I know how to present modal views in the new formSheet and a close approach can be found on this question: iPad iTunes Animation

    The problem is that you cannot choose where the animation will come from, so it just defaults and appears from the center, I want to customize it so that it appears from a specific location.

    The best example I can find for this animation can be seen on the first few seconds of this video

    If anyone can point me on the right direction using code, tutorials or documentation I would greatly appreciate it!

    Update:

    After some investigation I have found that this can be done using layers and Core Animation for the first part; and then animate it a formSheet modal view but I still dont quite understand how to achieve it, hopefully you guys can help!

    • Nurbol
      Nurbol almost 12 years
      The video linked in this question is no longer available, is there another video showing what you are looking for?
  • Zebs
    Zebs almost 13 years
    This does not allow you to choose where the animation will come from, so it defaults and apears from the center. I need to animate it in the same fashion you see on the video.
  • Hollance
    Hollance almost 13 years
    The basic idea is correct, though. You push the view controller with animated:NO and do your own animation. Mehul did this with a flip transition, which is easy but doesn't let you set a starting position. However, with a Core Animation block (instead of the [UIView beginAnimations] ... [commitAnimations]) you can do more complex animations including what you wish to accomplish.
  • thesummersign
    thesummersign almost 12 years
    hi, I tried your cod on my iPad app. I am adding a new view before calling animateWithCurrentView. the new view has a navigationbar with navigation bar button item after finishing the animation buttons are no more enabled ! what may be the problem?
  • kyleplattner
    kyleplattner almost 11 years
    I am trying to run the code above and I have a compiler warning where radians is used. It says "Implicit declaration of function 'radians' is invalid in C99." When I build it gives me a linker error: Undefined symbols for architecture armv7: "_radians", referenced from: -[MetricsPane metricTapped:] in MetricsPane.o (maybe you meant: _radiansToDegrees, _radiansToMeters ) ld: symbol(s) not found for architecture armv7. Any ideas why?
  • Anil Puttabuddhi
    Anil Puttabuddhi over 10 years
    sorry was not active for a long time on SO.. Try replacing radians(180) with M_PI and radians(0) with 0.
  • Anil Puttabuddhi
    Anil Puttabuddhi over 10 years
    Are you sure you can handle animating Rotation,scaling and translation along a path all at the same time using UIView animation alone? As far as I know, if all 3 things change at the same time, AnimationGroup becomes necessary. Correct me if am wrong and please provide me a sample code that animates all 3 parameters at once.
  • Hai Feng Kao
    Hai Feng Kao over 10 years
    Apple's doc indicates that it works for any animatable properties. It doesn't state that scale, rotation and translation cannot change at the same time. Where do you get this idea?
  • Anil Puttabuddhi
    Anil Puttabuddhi over 10 years
    Try animating a view whose size is (40,60) from point (0,10) to point (300,300), while flipping its view on the y axis (horizontal flip of 180 degrees), and the size changing to (20,30) and you will know what I mean. The frame changes even before the animations begin. Do give me a sample code that does that and I will accept that it is possible.
  • Anil Puttabuddhi
    Anil Puttabuddhi over 10 years
    @geekay You may have already figured out the solution. Change this line group.removedOnCompletion = NO to group.removedOnCompletion = YESand add/present/push your view with no animation.
  • Hai Feng Kao
    Hai Feng Kao over 10 years
    It works pretty well. Here is what I did: run Xcode 5, create a new project with single view application, copy my gist code to ViewController.m, Command-R to run the code. You shall see the view moving from (0,10) to (300,300) while resizing and flipping.