Search code examples
iosobjective-ccocoa-touchanimationcgaffinetransform

How to chain CGAffineTransform together in separate animations?


I need two animations on a UIView:

  1. Make the view move down and slightly grow.
  2. Make the view grow even bigger about its new center.

When I attempt to do that, the second animation starts in a weird location but ends up in the right location and size. How would I make the second animation start at the same position that the first animation ended in?

enter image description here

#import "ViewController.h"

static const CGFloat kStartX = 100.0;
static const CGFloat kStartY = 20.0;
static const CGFloat kStartSize = 30.0;
static const CGFloat kEndCenterY = 200.0;

@interface ViewController ()

@property (nonatomic, strong) UIView *box;

@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];

  self.box = [[UIView alloc] initWithFrame:CGRectMake(kStartX, kStartY, kStartSize, kStartSize)];
  self.box.backgroundColor = [UIColor brownColor];
  [self.view addSubview:self.box];

  [UIView animateWithDuration:2.0
                        delay:1.0
       usingSpringWithDamping:1.0
        initialSpringVelocity:0.0
                      options:0
                   animations:^{
                     self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
                   }
                   completion:^(BOOL finished) {
                     [UIView animateWithDuration:2.0
                                           delay:1.0
                          usingSpringWithDamping:1.0
                           initialSpringVelocity:0.0
                                         options:0
                                      animations:^{
                                        self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
                                      }
                                      completion:^(BOOL finished) {
                                      }];
                   }];
}

- (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
{
  CGFloat newScale = newSize / kStartSize;
  CGFloat startCenterY = kStartY + kStartSize / 2.0;
  CGFloat deltaY = newCenterY - startCenterY;
  CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
  CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
  return CGAffineTransformConcat(scaling, translation);
}

@end

There's one caveat: I'm forced to use setTransform rather than setFrame. I'm not using a brown box in my real code. My real code is using a complex UIView subclass that doesn't scale smoothly when I use setFrame.


Solution

  • This looks like it might be a UIKit bug with how UIViews resolve their layout when you apply a transform on top of an existing one. I was able to at least get the starting coordinates for the second animation correct by doing the following, at the very beginning of the second completion block:

      [UIView animateWithDuration:2.0
                            delay:1.0
           usingSpringWithDamping:1.0
            initialSpringVelocity:0.0
                          options:0
                       animations:^{
                         self.box.transform = [self _transformForSize:50.0 centerY:kEndCenterY];
                       }
                       completion:^(BOOL finished) {
                         // <new code here>
                         CGRect newFrame = self.box.frame;
                         self.box.transform = CGAffineTransformIdentity;
                         self.box.frame = newFrame;
                         // </new code>
                         [UIView animateWithDuration:2.0
                                               delay:1.0
                              usingSpringWithDamping:1.0
                               initialSpringVelocity:0.0
                                             options:0
                                          animations:^{
                                            self.box.transform = [self _transformForSize:100.0 centerY:kEndCenterY];
                                          }
                                          completion:^(BOOL finished) {
                                          }];
                       }];
    

    Using the same call to -_transformForSize:centerY: results in the same Y translation being performed in the second animation, though, so the box ends up further down in the view than you want when all is said and done.

    To fix this, you need to calculate deltaY based on the box's starting Y coordinate at the end of the first animation rather than the original, constant Y coordinate:

    - (CGAffineTransform)_transformForSize:(CGFloat)newSize centerY:(CGFloat)newCenterY
    {
      CGFloat newScale = newSize / kStartSize;
      // Replace this line:
      CGFloat startCenterY = kStartY + kStartSize / 2.0;
      // With this one:
      CGFloat startCenterY = self.box.frame.origin.y + self.box.frame.size.height / 2.0;
      // </replace>
      CGFloat deltaY = newCenterY - startCenterY;
      CGAffineTransform translation = CGAffineTransformMakeTranslation(0.0, deltaY);
      CGAffineTransform scaling = CGAffineTransformMakeScale(newScale, newScale);
      return CGAffineTransformConcat(scaling, translation);
    }
    

    and it should do the trick.

    UPDATE

    I should say, I consider this "trick" more of a work-around than an actual solution, but the box's frame and transform look correct at each stage of the animation pipeline, so if there's a true "solution" it's eluding me at the moment. This trick at least solves the translation problem for you, so you can experiment with your more complex view hierarchy and see how far you get.