CALayer Subclass Repeating Animation - ios

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]
}
}

Related

UIView animate not working on UIView's layers

I simply want to flash a red border around a UIView then fade out to clear, repeatedly. However, the UIView animate method doesn't seem to work.
Is there something special about the layer that prevent UIView animate from working?
public class MyAlertView: UIView {
convenience init(args: [String]) {
self.init(frame: CGRect.zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
self.translatesAutoresizingMaskIntoConstraints = false
self.layer.cornerRadius = 10
self.layer.borderColor = UIColor.systemRed.cgColor
self.layer.borderWidth = 2.5
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut, .repeat, .autoreverse], animations: {
self.layer.borderColor = UIColor.clear.cgColor
})
}
}
To animate layers need to use CAAnimations
u can use this extension
extension CALayer {
func addLoopBorderAnimation(from startColor: UIColor, to endColor: UIColor, withDuration duration: Double) {
let colorAnimation = CABasicAnimation(keyPath: "borderColor")
colorAnimation.fromValue = startColor.cgColor
colorAnimation.toValue = endColor.cgColor
colorAnimation.duration = duration
colorAnimation.repeatCount = .greatestFiniteMagnitude
colorAnimation.autoreverses = true
self.borderColor = endColor.cgColor
self.add(colorAnimation, forKey: "borderColor")
}
}
and call
private func commonInit() {
self.translatesAutoresizingMaskIntoConstraints = false
self.layer.cornerRadius = 10
self.layer.borderColor = UIColor.systemRed.cgColor
self.layer.borderWidth = 2.5
self.layer.addLoopBorderAnimation(from: UIColor.systemRed, to: UIColor.clear, withDuration: 0.5
}

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()

CAShapeLayer not visible in XIB swift iOS

I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: SpinningView!
//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let circularAnimation: CABasicAnimation = {
let animation = CABasicAnimation()
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
return animation
}()
//MARK: IB Inspectables
#IBInspectable var strokeColor: UIColor = UIColor.red {
...
}
#IBInspectable var trackColor: UIColor = UIColor.lightGray {
...
}
#IBInspectable var lineWidth: CGFloat = 5 {
...
}
#IBInspectable var fillColor: UIColor = UIColor.clear {
...
}
#IBInspectable var isAnimated: Bool = true {
...
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
initSubviews()
}
//MARK: Private functions
private func setup() {
setupTrack()
setupAnimationOnTrack()
}
private func setupTrack(){
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = trackColor.cgColor
layer.addSublayer(trackLayer)
}
private func setupAnimationOnTrack(){
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineCap = kCALineCapRound
layer.addSublayer(circleLayer)
updateAnimation()
}
private func updateAnimation() {
if isAnimated {
circleLayer.add(circularAnimation, forKey: "strokeEnd")
}
else {
circleLayer.removeAnimation(forKey: "strokeEnd")
}
}
//MARK: Layout contraints
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2
let startAngle = -CGFloat.pi / 2
let endAngle = 3 * CGFloat.pi / 2
let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackLayer.position = center
trackLayer.path = path.cgPath
circleLayer.position = center
circleLayer.path = path.cgPath
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)
When you are designing your XIB, it is not an instance of your #IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.
So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.
Take a look at this simple example:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var theLabel: UILabel!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
// this will change the label's textColor in Storyboard
// when a UIView's class is set to SpinningView
theLabel.textColor = .red
}
}
When I am designing the XIB, this is what I see:
But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:
The label's text color get's changed to red, because the code is now running.
Another way to think about it... you have trackColor, fillColor, etc vars defined as #IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.
Hope that makes sense.

Having custom CAShapeLayer animate with the Button

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.

Display a view of loading

I user Swift2 and Xcode 7.1
I want to show my view loading by calling a function. My view chragement is a class that inherits from UIView.
I would like my class is instantiated throughout my Controller automatically.
My class :
class loading: UIView {
let circle = UIView()
let anim = CAKeyframeAnimation(keyPath: "position")
init() { }
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func clear(){
self.hidden = true
// Animation ...
}
func display(){
self.hidden = false
// Animation ...
}
private func scaleAndColor(){ }
private func createAnim(){ }
private func createCircle(frame: CGRect){ }
}
What I would like to :
class ViewController: UIViewController {
// nothing to declare
override func viewDidLoad() {
super.viewDidLoad()
// nothing to do
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func anFunction(){
load() // display my loadView
clear() // clear my loadView
}
}
Here's a Loader Singleton I built in Swift 2.1
import Foundation
import UIKit
class MGSwiftLoader: UIView {
static let sharedInstance = MGSwiftLoader(frame: CGRectZero)
private var backgroundView: UIView!
private var label: UILabel!
private let activityIndicator = UIActivityIndicatorView()
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
//See through view
self.backgroundColor = UIColor.clearColor()
self.opaque = false
//BackgroundView will contain the UIVisualEffectView, label and activityIndicator
backgroundView = UIView()
backgroundView.backgroundColor = UIColor.clearColor()
backgroundView.opaque = false
backgroundView.layer.cornerRadius = 5.0
self.addSubview(backgroundView)
backgroundView.snp_makeConstraints { (make) -> Void in
make.center.equalTo(self)
make.height.equalTo(100.0)
}
//Blur
let blurEffect = UIBlurEffect(style: .Light)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
blurredEffectView.layer.cornerRadius = 5.0
blurredEffectView.clipsToBounds = true
backgroundView.addSubview(blurredEffectView)
blurredEffectView.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(backgroundView)
}
//ActivityIndicator
activityIndicator.color = UIColor.lightGrayColor()
backgroundView.addSubview(activityIndicator)
activityIndicator.transform = CGAffineTransformMakeScale(1.15, 1.15)
activityIndicator.snp_makeConstraints { (make) -> Void in
make.center.equalTo(backgroundView)
}
}
//Shows the loder - The view takes all the screen disabling touches but we only see the backgroundView with its subviews
func show() {
self.hidden = false
//Take all of the MainWindow screen
let application = UIApplication.sharedApplication()
let frontWindow = application.keyWindow
frontWindow?.addSubview(self)
self.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(frontWindow!)
}
activityIndicator.startAnimating()
//Show top activity indicator
application.networkActivityIndicatorVisible = true
//Scale animation - you can add any animation you want
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSNumber(float: 0.0)
scaleAnimation.toValue = NSNumber(float: 1.0)
scaleAnimation.duration = 0.33
scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
self.layer.addAnimation(scaleAnimation, forKey: "scaleAnimation")
}
func showWithSubTitle(text: String) {
show()
if label != nil {
label.removeFromSuperview()
}
activityIndicator.snp_remakeConstraints { (make) -> Void in
make.centerX.equalTo(backgroundView)
make.centerY.equalTo(backgroundView).offset(-10.0)
}
label = UILabel()
label.textColor = UIColor.lightGrayColor()
label.textAlignment = NSTextAlignment.Center
label.text = text
label.customFont(.Light, size: 15.0)
backgroundView.addSubview(label)
label.snp_makeConstraints { (make) -> Void in
make.bottom.equalTo(backgroundView).offset(-10.0)
make.right.equalTo(backgroundView).offset(-20.0)
make.left.equalTo(backgroundView).offset(20.0)
}
}
func hide() {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSNumber(float: 1.0)
scaleAnimation.toValue = NSNumber(float: 0.0)
scaleAnimation.duration = 0.33
scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
scaleAnimation.delegate = self
scaleAnimation.fillMode = kCAFillModeForwards
scaleAnimation.removedOnCompletion = false
self.layer.addAnimation(scaleAnimation, forKey: "scaleAnimation")
self.hidden = true
//Hide top activity indicator
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
You just use it by calling:
MGSwiftLoader.sharedInstance.show()
or
MGSwiftLoader.sharedInstance.showWithSubTitle("Your Loader Text")
And hide it with:
MGSwiftLoader.sharedInstance.hide()
Keep in mind that I'm using SnapKit to build the constrains manually (http://snapkit.io)

Resources