Search code examples
objective-ccocoa-touchcore-animationuiviewanimation

How to synchronously animate a UIView and a CALayer


To illustrate this question, I gisted a very small Xcode project on Github (two classes, 11kb download). Have a look at the gist here or use git clone [email protected]:93982af3b65d2151672e.git.


Please consider the following scenario. A custom view of mine, called 'container view', contains two little squares. Illustrated in this screenshot:

Screenshot of the initial position of the two squares

The blue square is a 22x22 pt UIView instance and the red square is a 22x22 pt CALayer instance. For purposes of this question I want the two squares to 'stick' to the bottom right corner of the outer view, while I animate the frame of that outer view.

I change the container view's frame within a UIView's class method animateWithDuration:delay:options:animations:completion:, with a non-default easing parameter of type UIViewAnimationCurveEaseOut.

[UIView animateWithDuration:1.0 delay:0 
                    options:UIViewAnimationCurveEaseOut 
                 animations:^{ _containerView.frame = randomFrame; } 
                 completion:nil];

Note: Both the blue UIView's and the red CALayer's frame are set in the overridden setFrame: method of the container view, but results would have been the same if I had set the blue UIView's autoresizingMask property to UIView flexible left and top margins.

In the resulting animation, the blue square 'sticks' to the corner just the way I intended it, but the red square completely disregards both timing and easing of the animation. I assume this is because of the implicit animation feature of Core Animation, a feature that has helped me in many occasions before.

Here are a few screenshots in the animation sequence that illustrate the asynchronicity of the two squares:

Three screenshots mid-animation where the red square is not in the 'right' place

On to the question: Is there a way to synchronize the two frame changes so that the red CALayer and the blue UIView both move with the same animation duration and easing curve, sticking to each other as if they were one view?

P.S. Of course the required visual result of the two squares sticking together could be achieved in any number of ways, for example by having both layers become either CALayers or UIViews, but the project that the real issue is in has a very legit cause for the one to be a CALayer and the other to be a UIView.


Solution

  • I'm assuming that you got the end position correct and that after the animation that the view and the layer are aligned. That has nothing to do with the animation, just geometry.


    When you change the property of a CALayer that isn't the layer of a view (like in your case) it will implicitly animate to its new value. To customize this animation you could use an explicit animation, like a basic animation. Changing the frame in a basic animation would look something like this.

    CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:@"frame"];
    [myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
    [myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
    [myAnimation setDuration:1.0];
    [myAnimation setTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
    
    [myLayer setFrame:myNewFrame];
    [myLayer addAnimation:myAnimation forKey:@"someKeyForMyAnimation"];
    

    If the timing function and the duration of both animations are the same then they should stay aligned.

    Also note that explicit animations doesn't change the value and hat you have to both add the animation and set the new value (like in the sample above)


    Yes, there are a number of ways to achieve the same effect. One example is having the view and the layer be subviews of the same subview (that in turn is a subview of the outer frame).

    Edit

    You can't easily group the UIView-animation with an explicit animation. Neither can you use an animation group (since you are applying them to different layers) but yo can use a CATransaction:

    [CATransaction begin];
    [CATransaction setAnimationDuration:1.0];
    [CATransaction setAnimationTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
    
    // Layer animation
    CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:@"frame"];
    [myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
    [myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
    
    [myLayer setFrame:myNewFrame];
    [myLayer addAnimation:myAnimation forKey:@"someKeyForMyAnimation"];
    
    // Outer animation
    CABasicAnimation *outerAnimation = [CABasicAnimation animationWithKeyPath:@"frame"];
    [outerAnimation setToValue:[NSValue valueWithCGRect:myNewOuterFrame]];
    [outerAnimation setFromValue:[NSValue valueWithCGRect:[outerView frame]]];
    
    [[outerView layer] setFrame:myNewOuterFrame];
    [[outerView layer] addAnimation:outerAnimation forKey:@"someKeyForMyOuterAnimation"];
    
    [CATransaction commit];