Search code examples
iosios6ios7

Animation misalignment on ios6


I have a slide segue that works fine in ios7, but has a jump in animation positioning for ios6. The difference seems to be due to the status bar on ios6 - as the difference in position matches the status bar height. I can fudge the layer positions to make it work, but it feels hacky - and I'd like to understand why it is happening.

Pausing the animation and inspecting the layer hierarchy on ios6 gives this:

<UIWindow: [The root window]
    frame = (0 0; 320 568); 
    layer = <UIWindowLayer: 0x8127b50; frame = (0 0; 320 568)>
>
   | <UIView: [The root view]
   |     frame = (0 0; 320 548); 
   |     layer = <CALayer: frame = (0 0; 320 548)>
   | >
   |    | <CALayer: frame = (0 0; 320 548)> [Animation is happening in this layer]
   |    |    | <CALayer: frame = (1 0; 320, 548)> 
   |    |    | <CALayer: frame = (321 0; 320 548)> 

When the animation is complete the layer hierarchy looks like this:

<UIWindow:  [The root window]
    frame = (0 0; 320 568); 
    layer = <UIWindowLayer: frame = (0 0; 320 568)>
 >
   | <UIView: [The root view]
   |     frame = (0 20; 320 548);
   |     layer = <CALayer: frame = (0 20; 320 548)>
   | >

In ios7 the non-animated root views layer position is just (0 0 ...), rather than (0 20 ...) which I assume is just transparency of the status bar.

So something is moving the root views layer position on ios6, but I'm not sure why. I'm probably doing something stupid, but I just cant see it. Any pointers would be greatly appreciated.

#import "ANBSlideSegue.h"
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>


@implementation ANBSlideSegue


-(UIImage*) screenShot:(UIView*)aView color:(UIColor*)color {

  //This will only work on the main thread, so lets assert that here..
  NSAssert([NSThread isMainThread], @"screenShot only works on the main thread...");

  //This should no longer be needed - we set the view frames on construction.
  //Since this may not have been layed out .. we force a layout here.
  CGRect frame = (CGRect){.size = hostView.frame.size};

  CGRect imageFrame = frame;

  aView.frame = frame;

  [aView layoutIfNeeded];

  UIGraphicsBeginImageContextWithOptions(imageFrame.size, NO, 0.0);

  CGContextSaveGState(UIGraphicsGetCurrentContext());

  //Adjust for the frame
  CGContextTranslateCTM(UIGraphicsGetCurrentContext(),
                        aView.frame.origin.x,
                        aView.frame.origin.y);


  [aView.layer renderInContext:UIGraphicsGetCurrentContext()];

  CGContextRestoreGState(UIGraphicsGetCurrentContext());
  CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), color.CGColor);
  CGContextFillRect(UIGraphicsGetCurrentContext(), imageFrame);
  UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();

  return image;
}

-(CALayer*) createLayerFromView:(UIView*) aView transform:(CATransform3D) transform image:(UIImage*) shot {
  CALayer* imageLayer = [CALayer layer];
  imageLayer.anchorPoint = CGPointMake(1.0f, 1.0f);
  imageLayer.frame = (CGRect){.size = hostView.frame.size};
  imageLayer.transform = transform;
  imageLayer.contents = (__bridge id)shot.CGImage;
  return imageLayer;
}

-(void) animationDidStart:(CAAnimation *)anim {
}

-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
  [transformationLayer removeAnimationForKey:@"TransitionViewAnimation"];
  [transformationLayer removeFromSuperlayer];

  UIViewController * parent = ((UIViewController *)self.sourceViewController).presentingViewController;
  if(parent) {
    [self.sourceViewController dismissViewControllerAnimated:NO completion:NULL];
    [parent presentViewController:self.destinationViewController animated:NO completion:NULL];
  } else if(self.sourceViewController == UIApplication.sharedApplication.keyWindow.rootViewController) {
    UIApplication.sharedApplication.keyWindow.rootViewController = self.destinationViewController;
  } else {
    NSLog(@"Error performing finalization of segue!");
  }
}

-(void)animateWithDuration:(CGFloat) aDuration {

  CGFloat width = hostView.frame.size.width;
  CGFloat height = hostView.frame.size.height;
  float multiplier = _slideLeft ? -1.0f : 1.0f;

  CABasicAnimation *translation = nil;


  translation = [CABasicAnimation animationWithKeyPath:@"sublayerTransform.translation.x"];
  translation.toValue = [NSNumber numberWithFloat:multiplier * width];

  translation.fillMode = kCAFillModeForwards;
  translation.removedOnCompletion = NO;
  translation.delegate = self;
  translation.duration = aDuration;

  [CATransaction flush];
  [transformationLayer addAnimation:translation forKey:@"TransitionViewAnimation"];
}

- (void) constructTranslationLayer
{

  UIViewController *source = (UIViewController *) self.sourceViewController;
  UIViewController *dest = (UIViewController *) self.destinationViewController;

  hostView = source.view;

  //For rotated views we need to hack in the transformations and size to match too..
  dest.view.transform = source.view.transform;
  dest.view.frame = source.view.frame;

  UIImage * sourceShot = [self screenShot:source.view color:[UIColor.blackColor colorWithAlphaComponent:0.2f]];
  UIImage * destShot   = [self screenShot:dest.view   color:UIColor.clearColor];

  dest.view.layer.borderWidth=2.0f; dest.view.layer.borderColor= [[UIColor whiteColor] CGColor];


  transformationLayer = [CALayer layer];
  transformationLayer.borderWidth=2.0f; transformationLayer.borderColor= [[UIColor redColor] CGColor];

  transformationLayer.frame = hostView.bounds;
  transformationLayer.anchorPoint = CGPointMake(0.5f, 0.5f);
  CATransform3D sublayerTransform = CATransform3DIdentity;
  [transformationLayer setSublayerTransform:sublayerTransform];
  [hostView.layer addSublayer:transformationLayer];

  CATransform3D transform = CATransform3DMakeTranslation(1.0, 0, 0);

  CALayer * sourceLayer = [self createLayerFromView:source.view transform:transform image:sourceShot ];

  [transformationLayer addSublayer:sourceLayer];

  //If we're rotated we need to adjust the direction
  UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];

  CGFloat scale = (_slideLeft)?1.0:-1.0;

  transform = CATransform3DTranslate(transform, scale*hostView.frame.size.width, 0, 0);

  CALayer * destLayer =[self createLayerFromView:dest.view transform:transform image:destShot];
  [transformationLayer addSublayer:destLayer];

}

- (void)perform {
  [self constructTranslationLayer];
  [self animateWithDuration:3.0f];
}

@end

Solution

  • Turns out the issue was some stupidity in the screenshot function, which was adjusting the frame of the layer before taking the screenshot, but not restoring it. Turns out the changes to the frame were no-longer necessary, so the function can just look like this:

    -(UIImage*) screenShot:(UIView*)aView color:(UIColor*)color {
    
      //Since this may not have been layed out .. we force a layout here.
      [aView layoutIfNeeded];
    
      //This will only work on the main thread, so lets assert that here..
      NSAssert([NSThread isMainThread], @"screenShot only works on the main thread...");
    
      CGRect frame = (CGRect){.size = hostView.frame.size};
    
      UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0);
    
      CGContextSaveGState(UIGraphicsGetCurrentContext());
      [aView.layer renderInContext:UIGraphicsGetCurrentContext()];
      CGContextRestoreGState(UIGraphicsGetCurrentContext());
      CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), color.CGColor);
      CGContextFillRect(UIGraphicsGetCurrentContext(), frame);
      UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
    
      return image;
    }