CALayer does not animate the same as the UIView animation
Animating layer properties in sync with UIView animation can be tricky. Instead, let's use a different view structure to take advantage of the built-in support for animating a view's transform
, instead of trying to animate a layer's path
.
We'll use two views: a superview and a subview. The superview is a LogoView
and is what we lay out in the storyboard (or however you create your UI). The LogoView
adds a subview to itself of class LogoLayerView
. This LogoLayerView
uses a CAShapeLayer
as its layer instead of a plain CALayer
.
Note that we only need one CAShapeLayer
, because a path can contain multiple disconnected regions.
We set the frame/bounds of the LogoLayerView
once, to CGRectMake(0, 0, 213, 213)
, and never change it. Instead, when the outer LogoView
changes size, we set the LogoLayerView
's transform
so that it still fills the outer LogoView
.
Here's the result:
Here's the code:
LogoView.h
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface LogoView : UIView
@end
LogoView.m
#import "LogoView.h"
#define CGRECTMAKE(a, b, w, h) {.origin={.x=(a),.y=(b)},.size={.width=(w),.height=(h)}}
const static CGRect ovalRects[] = {
CGRECTMAKE(62.734375,-21.675000,18.900000,18.900000),
CGRECTMAKE(29.784375,-31.725000,27.400000,27.300000),
CGRECTMAKE(2.534375,-81.775000,18.900000,18.900000),
CGRECTMAKE(4.384375,-57.225000,27.400000,27.300000),
CGRECTMAKE(2.784375,62.875000,18.900000,18.900000),
CGRECTMAKE(4.334375,29.925000,27.400000,27.300000),
CGRECTMAKE(62.734375,2.525000,18.900000,18.900000),
CGRECTMAKE(29.784375,4.475000,27.400000,27.300000),
CGRECTMAKE(-21.665625,-81.775000,18.900000,18.900000),
CGRECTMAKE(-31.765625,-57.225000,27.400000,27.300000),
CGRECTMAKE(-81.615625,-21.425000,18.900000,18.900000),
CGRECTMAKE(-57.215625,-31.775000,27.400000,27.300000),
CGRECTMAKE(-81.615625,2.775000,18.900000,18.900000),
CGRECTMAKE(-57.215625,4.425000,27.400000,27.300000),
CGRECTMAKE(-21.415625,62.875000,18.900000,18.900000),
CGRECTMAKE(-31.765625,29.925000,27.400000,27.300000)
};
#define LogoDimension 213.0
@interface LogoLayerView : UIView
@property (nonatomic, strong, readonly) CAShapeLayer *layer;
@end
@implementation LogoLayerView
@dynamic layer;
+ (Class)layerClass {
return [CAShapeLayer class];
}
- (void)layoutSubviews {
[super layoutSubviews];
if (self.layer.path == nil) {
[self initShapeLayer];
}
}
- (void)initShapeLayer {
self.layer.backgroundColor = [UIColor yellowColor].CGColor;
self.layer.strokeColor = nil;
self.layer.fillColor = [UIColor greenColor].CGColor;
UIBezierPath *path = [UIBezierPath bezierPath];
for (size_t i = 0; i < sizeof ovalRects / sizeof *ovalRects; ++i) {
[path appendPath:[UIBezierPath bezierPathWithOvalInRect:ovalRects[i]]];
}
[path applyTransform:CGAffineTransformMakeTranslation(LogoDimension / 2, LogoDimension / 2)];
self.layer.path = path.CGPath;
}
@end
@implementation LogoView {
LogoLayerView *layerView;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (layerView == nil) {
layerView = [[LogoLayerView alloc] init];
layerView.layer.anchorPoint = CGPointZero;
layerView.frame = CGRectMake(0, 0, LogoDimension, LogoDimension);
[self addSubview:layerView];
}
[self layoutShapeLayer];
}
- (void)layoutShapeLayer {
CGSize mySize = self.bounds.size;
layerView.transform = CGAffineTransformMakeScale(mySize.width / LogoDimension, mySize.height / LogoDimension);
}
@end
You can make CAShapeLayer.path
animatable and update your custom layer in -layoutSublayersOfLayer
. Just make sure to match duration of UIView
and CAShapeLayer
subclass.
Simple and straightforward:
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let kAnimationDuration: NSTimeInterval = 4.0
class AnimatablePathShape: CAShapeLayer {
override func actionForKey(event: String) -> CAAction? {
if event == "path" {
let value = self.presentationLayer()?.valueForKey(event) ?? self.valueForKey(event)
let anim = CABasicAnimation(keyPath: event)
anim.duration = kAnimationDuration
anim.fromValue = value
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
return anim
}
return super.actionForKey(event)
}
override class func needsDisplayForKey(key: String) -> Bool {
if key == "path" {
return true
}
return super.needsDisplayForKey(key)
}
}
class View: UIView {
let shape = AnimatablePathShape()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.1)
self.shape.fillColor = UIColor.magentaColor().CGColor
self.layer.addSublayer(self.shape)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func yoyo(grow: Bool = true) {
let options: UIViewAnimationOptions = [.CurveEaseInOut]
let animations = { () -> Void in
let scale: CGFloat = CGFloat(grow ? 4 : 1.0 / 4)
self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame) * scale, CGRectGetHeight(self.frame) * scale)
}
let completion = { (finished: Bool) -> Void in
self.yoyo(!grow)
}
UIView.animateWithDuration(kAnimationDuration, delay: 0, options: options, animations: animations, completion: completion)
}
override func layoutSublayersOfLayer(layer: CALayer) {
super.layoutSublayersOfLayer(layer)
let radius = min(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame)) * 0.25
let center = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI * 2), clockwise: true)
self.shape.path = bezierPath.CGPath
}
}
let view = View(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.addSubview(view)
view.yoyo()
XCPlaygroundPage.currentPage.liveView = container