Having custom CAShapeLayer animate with the Button - ios

I'm animating my button by changing a constraint of my auto layout and using an UIView animation block to animate it:
UIView.animateWithDuration(0.5, animations: { self.layoutIfNeeded() })
In this animation, only the width of the button is changing and the button itself is animating.
In my button, there's a custom CAShapeLayer. Is it possible to catch the animation of the button and add it to the layer so it animates together with the button?
What I've Tried:
// In my CustomButton class
override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? {
if event == "bounds" {
if let action = super.actionForLayer(layer, forKey: "bounds") as? CABasicAnimation {
let animation = CABasicAnimation(keyPath: event)
animation.fromValue = border.path
animation.toValue = UIBezierPath(rect: bounds).CGPath
// Copy values from existing action
border.addAnimation(animation, forKey: nil) // border is my CAShapeLayer
}
return super.actionForLayer(layer, forKey: event)
}
// In my CustomButton class
override func layoutSubviews() {
super.layoutSubviews()
border.frame = layer.bounds
let fromValue = border.path
let toValue = UIBezierPath(rect: bounds).CGPath
CATransaction.setDisableActions(true)
border.path = toValue
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = fromValue
animation.toValue = toValue
animation.duration = 0.5
border.addAnimation(animation, forKey: "animation")
}
Nothing is working, and I've been struggling for days..
CustomButton:
class CustomButton: UIButton {
let border = CAShapeLayer()
init() {
super.init(frame: CGRectZero)
translatesAutoresizingMaskIntoConstraints = false
border.fillColor = UIColor.redColor().CGColor
layer.insertSublayer(border, atIndex: 0)
}
// override func layoutSubviews() {
// super.layoutSubviews()
//
// border.frame = layer.bounds
// border.path = UIBezierPath(rect: bounds).CGPath
// }
override func layoutSublayersOfLayer(layer: CALayer) {
super.layoutSublayersOfLayer(layer)
border.frame = self.bounds
border.path = UIBezierPath(rect: bounds).CGPath
}
}

All you have to do is resize also the sublayers when the backing layer of your view is resized. Because of implicit animation the change should be animated. So all you need to do is basically to set this in you custom view class:
override func layoutSublayersOfLayer(layer: CALayer!) {
super.layoutSublayersOfLayer(layer)
border.frame = self.bounds
}
Updated
I had some time to play with the animation and it seems to work for me now. This is how it looks like:
class TestView: UIView {
let border = CAShapeLayer()
init() {
super.init(frame: CGRectZero)
translatesAutoresizingMaskIntoConstraints = false
border.backgroundColor = UIColor.redColor().CGColor
layer.insertSublayer(border, atIndex: 0)
border.frame = CGRect(x: 0, y:0, width: 60, height: 60)
backgroundColor = UIColor.greenColor()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSublayersOfLayer(layer: CALayer) {
super.layoutSublayersOfLayer(layer)
CATransaction.begin();
CATransaction.setAnimationDuration(10.0);
border.frame.size.width = self.bounds.size.width
CATransaction.commit();
}
}
And I use it like this:
var tview: TestView? = nil
override func viewDidLoad() {
super.viewDidLoad()
tview = TestView();
tview!.frame = CGRect(x: 100, y: 100, width: 60, height: 60)
view.addSubview(tview!)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tview!.frame.size.width = 200
}
The issue is that the frame property of CALayer is not animable. The docs say:
Note:Note
The frame property is not directly animatable. Instead you should animate the appropriate combination of the bounds, anchorPoint and position properties to achieve the desired result.
If you didn't solve your problem yet or for others that might have it I hope this helps.

Related

ios animation - display image/fill color from left to right with animating

I have a image, been added to CALayer.contents.
The image basically contains a word/letters, the requirement is to animate like showing up letters from left to right gradually, mean while animating the image positions as like initially 1st letter on the middle and eventually whole image been at central position.
Any suggestion on how to achieve this with Core animation? Code snippets will be greatly appreciated.
One thought I had is to set the image colour same as background colour at initial state(looks transparent), and fill image colour to desired colour from left to right with animation. And at the same time animate the image position.
You can do this by using a gradient layer as a mask, then animating the position of the image layer and the locations of the gradient.
Here's a quick example to get started:
class RevealView: UIView {
public var image: UIImage? {
didSet {
imgLayer.contents = image?.cgImage
}
}
public var duration: TimeInterval = 1.0
private let gradLayer = CAGradientLayer()
private let imgLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
layer.addSublayer(imgLayer)
imgLayer.contentsGravity = .resize
// white area shows through, clear area is "hidden"
gradLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
// left-to-right gradient
gradLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
// start locations so entire layer is masked
gradLayer.locations = [0.0, 0.0]
// set the mask
layer.mask = gradLayer
}
override func layoutSubviews() {
super.layoutSubviews()
// set image layer frame to view bounds
imgLayer.frame = bounds
// move it half-way to the right
imgLayer.position.x = bounds.maxX
// set gradient layer frame to view bounds
gradLayer.frame = bounds
}
public func doAnim() {
let imgAnim = CABasicAnimation(keyPath: "position.x")
let gradAnim = CABasicAnimation(keyPath: "locations")
// animate image layer from right-to-left
imgAnim.toValue = bounds.midX
// animate gradient from left-to-right
gradAnim.toValue = [NSNumber(value: 1.0), NSNumber(value: 2.0)]
imgAnim.duration = self.duration
gradAnim.duration = imgAnim.duration * 2.0
[imgAnim, gradAnim].forEach { anim in
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards
anim.beginTime = CACurrentMediaTime()
}
CATransaction.begin()
CATransaction.setDisableActions(true)
imgLayer.add(imgAnim, forKey: nil)
gradLayer.add(gradAnim, forKey: nil)
CATransaction.commit()
}
}
class RevealVC: UIViewController {
let testView = RevealView()
override func viewDidLoad() {
super.viewDidLoad()
guard let img = UIImage(named: "mt") else {
fatalError("Could not load image!")
}
testView.image = img
// use longer or shorter animation duration if desired (default is 1.0)
//testView.duration = 3.0
testView.translatesAutoresizingMaskIntoConstraints = false
testView.backgroundColor = .clear
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.widthAnchor.constraint(equalToConstant: img.size.width),
testView.heightAnchor.constraint(equalToConstant: img.size.height),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testView.doAnim()
}
}

Animate UIView's Layer with constrains (Auto Layout Animations)

I am working on project where I need to animate height of view which consist of shadow, gradient and rounded corners (not all corners).
So I have used layerClass property of view.
Below is simple example demonstration.
Problem is that, when I change height of view by modifying its constraint, it was resulting in immediate animation of layer class, which is kind of awkward.
Below is my sample code
import UIKit
class CustomView: UIView{
var isAnimating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
func setupView(){
self.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
guard let layer = self.layer as? CAShapeLayer else { return }
layer.fillColor = UIColor.green.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
layer.shadowOpacity = 0.6
layer.shadowRadius = 5
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
// While animating `myView` height, this method gets called
// So new bounds for layer will be calculated and set immediately
// This result in not proper animation
// check by making below condition always true
if !self.isAnimating{ //if true{
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
}
}
class TestViewController : UIViewController {
// height constraint for animating height
var heightConstarint: NSLayoutConstraint?
var heightToAnimate: CGFloat = 200
lazy var myView: CustomView = {
let view = CustomView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
lazy var mySubview: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var button: UIButton = {
let button = UIButton(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Animate", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(self.animateView(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.myView)
self.myView.addSubview(self.mySubview)
self.view.addSubview(self.button)
self.myView.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor).isActive = true
self.myView.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor).isActive = true
self.myView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor, constant: 100).isActive = true
self.heightConstarint = self.myView.heightAnchor.constraint(equalToConstant: 100)
self.heightConstarint?.isActive = true
self.mySubview.leadingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.leadingAnchor).isActive = true
self.mySubview.trailingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.trailingAnchor).isActive = true
self.mySubview.topAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.topAnchor).isActive = true
self.mySubview.bottomAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.bottomAnchor).isActive = true
self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.button.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor, constant: -20).isActive = true
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.myView.isAnimating = true
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
}
}
While animating view, it will call its layoutSubviews() method, which will result into recalculating bounds of shadow layer.
So I checked if view is currently animating, then do not recalculate bounds of shadow layer.
Is this approach right ? or there is any better way to do this ?
I know it's a tricky question. Actually, you don't need to care about layoutSubViews at all. The key here is when you set the shapeLayer. If it's setup well, i.e. after the constraints are all working, you don't need to care that during the animation.
//in CustomView, comment out the layoutSubViews() and add updateLayer()
func updateLayer(){
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: layer.bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
// override func layoutSubviews() {
// super.layoutSubviews()
//
// // While animating `myView` height, this method gets called
// // So new bounds for layer will be calculated and set immediately
// // This result in not proper animation
//
// // check by making below condition always true
//
// if !self.isAnimating{ //if true{
// guard let layer = self.layer as? CAShapeLayer else { return }
//
// layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
// layer.shadowPath = layer.path
// }
// }
in ViewController: add viewDidAppear() and remove other is animation block
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myView.updateLayer()
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
....
Then you are good to go. Have a wonderful day.
Below code also worked for me, As I want to use layout subviews without any flags.
UIView.animate(withDuration: 5.0, animations: {
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: self.heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.heightConstarint?.constant = self.heightToAnimate
self.myView.layoutIfNeeded()
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
})
And overriding layoutSubview as follows
override func layoutSubviews() {
super.layoutSubviews()
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}

Animate setFillColor color change in custom UIView

I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()

Change color of a custom UIButton on .selected state

I have designed a radial gradient button for my app's home screen:
class RadialGradientButton: UIButton {
var insideColor: UIColor = ColorScheme.limeGreen
var outsideColor: UIColor = UIColor.white
var gradientLocation: CGFloat = 0.9
override func draw(_ rect: CGRect) {
let colors = [insideColor.cgColor, outsideColor.cgColor] as CFArray
let center = CGPoint(x: rect.size.width / 2, y: rect.size.height / 2)
let endRadius = rect.size.width / 2
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: [gradientLocation, 1.0])
UIGraphicsGetCurrentContext()!.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: .drawsBeforeStartLocation)
}
}
Now with this button I've ran into a user experience issue: when the button is tapped, its appearance is not changed in any way because of gradient color settings. I want it to behave like any system button - that is, insideColor should get darker on .selected state.
I've tried to play around with setNeedsDisplay() when Touch Down and Touch Up Inside events are triggered, but this method does not perform immediate redrawing, plus can be harsh on performance.
Maybe someone here can lead me in a right direction.
draw() method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. Overriding this method, UIKit creates and configures a graphics context for drawing and render your gradient layer on it. That's why your selected background is not working.
You can achieve this behaviour by insert two layer on your view- one for highlighted and another for normal state. Later change behaviour on touch event.
import UIKit
import QuartzCore
class RoundedButton: UIButton {
var gradientBackgroundLayer : CAGradientLayer?
var selectedLayer : CALayer?
var insideColor: UIColor = UIColor.green;
var outsideColor: UIColor = UIColor.white;
var gradientLocation: NSNumber = 0.9;
let kAnimationLength:CGFloat = 0.15;
override init(frame: CGRect) {
super.init(frame: frame);
commonInit();
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
commonInit();
}
override func awakeFromNib() {
super.awakeFromNib();
commonInit();
}
func commonInit() {
self.backgroundColor = nil;
self.clipsToBounds = true;
self.setTitleColor(UIColor.white, for: UIControlState.normal)
let colors : [CGColor] = [insideColor.cgColor, outsideColor.cgColor];
let locations : [NSNumber] = [0,gradientLocation];
let layer : CAGradientLayer = CAGradientLayer();
layer.frame = self.bounds;
layer.colors = colors;
layer.locations = locations;
self.gradientBackgroundLayer = layer;
let selectedLayer : CALayer = CALayer();
selectedLayer.frame = self.bounds;
selectedLayer.backgroundColor = UIColor.lightGray.cgColor
self.selectedLayer = selectedLayer;
}
func performLayout() {
self.gradientBackgroundLayer?.frame = self.bounds;
self.selectedLayer?.frame = self.bounds;
self.layer.insertSublayer(self.gradientBackgroundLayer!, at: 1);
self.layer.insertSublayer(self.selectedLayer!, at: 0);
self.layer.cornerRadius = self.frame.size.height / 2.0;
}
override func layoutSubviews() {
super.layoutSubviews();
performLayout();
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event);
UIView.animate(withDuration: TimeInterval(kAnimationLength), delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.isHighlighted = true;
self.selectedLayer?.isHidden = false;
self.gradientBackgroundLayer?.isHidden = true;
}, completion: nil);
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event);
UIView.animate(withDuration: TimeInterval(kAnimationLength), delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.isHighlighted = false;
self.gradientBackgroundLayer?.isHidden = false;
self.selectedLayer?.isHidden = true;
}, completion: nil);
}
}
Try this. Hope it will work.
when touch Up Inside.
if self.mButton.isSelected = true {
self.mButton.isSelected = false
self.mButton.setBackgroundColor(UIColor.red, forState: .normal)
}
else{
self.mButton.isSelected = true
self.mButton.setBackgroundColor(UIColor.blue, forState: .normal)
}
and you can use this code in viewDidLoad()

CALayer Subclass Repeating Animation

I'm attempting to create a CALayer subclass that performs an animation every x seconds. In the example below I'm attempting to change the background from one random color to another but when running this in the playground nothing seems to happen
import UIKit
import XCPlayground
import QuartzCore
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200, height: 200))
XCPShowView("view", view)
class CustomLayer: CALayer {
var colors = [
UIColor.blueColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.yellowColor().CGColor
]
override init!() {
super.init()
self.backgroundColor = randomColor()
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = backgroundColor
animation.toValue = randomColor()
animation.duration = 3.0
animation.repeatCount = Float.infinity
addAnimation(animation, forKey: "backgroundColor")
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func randomColor() -> CGColor {
let index = Int(arc4random_uniform(UInt32(colors.count)))
return colors[index]
}
}
let layer = CustomLayer()
layer.frame = view.frame
view.layer.addSublayer(layer)
The parameters of a repeating animation are only setup once, so you can't change the color on each repeat. Instead of a repeating animation, you should implement the delegate method,
animationDidStop:finished:, and call the animation again from there with a new random color. I haven't tried this in a playground, but it works ok in an app. Notice that you have to implement init!(layer layer: AnyObject!) in addition to the other init methods you had.
import UIKit
class CustomLayer: CALayer {
var newColor: CGColorRef!
var colors = [
UIColor.blueColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.yellowColor().CGColor
]
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init!(layer: AnyObject!) {
super.init(layer: layer)
}
override init!() {
super.init()
backgroundColor = randomColor()
newColor = randomColor()
self.animateLayerColors()
}
func animateLayerColors() {
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = backgroundColor
animation.toValue = newColor
animation.duration = 3.0
animation.delegate = self
addAnimation(animation, forKey: "backgroundColor")
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
backgroundColor = newColor
newColor = randomColor()
self.animateLayerColors()
}
private func randomColor() -> CGColor {
let index = Int(arc4random_uniform(UInt32(colors.count)))
return colors[index]
}
}

Resources