UIView animate not working on UIView's layers - ios

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
}

Related

issue adding layer around a circular view's border

I am adding a circular layer around my circular view and setting its position to be the center of the view but the layer gets added at a different position. The following is the code.
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerFrame = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.frame = outerLayerFrame
let path = UIBezierPath(ovalIn: outerLayerFrame)
outerLayer.path = path.cgPath
outerLayer.position = self.center
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}
Can anyone please tell me what is wrong here.
You want to set your layer(s) properties when the view is instantiated / initialized, but its size can (and usually does) change between then and when auto-layout adjusts it to the current device dimensions.
Unlike the UIView itself, layers do not auto-resize.
So, you want to set framing and layer paths in layoutSubviews():
class CustomView: UIView {
let outerLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// layoutSubviews is where you want to set your size
// and corner radius values
override func layoutSubviews() {
super.layoutSubviews()
// make self "round"
self.layer.cornerRadius = self.bounds.height/2
// add a round path for outer layer
let path = UIBezierPath(ovalIn: bounds)
outerLayer.path = path.cgPath
}
private func configure() {
self.backgroundColor = UIColor.systemBlue
// setup layer properties here
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
// add the sublayer here
self.layer.addSublayer(outerLayer)
}
}
Result (with the view set to 120 x 120 pts):
So, I realized, it's the position that is making the layer not aligned in center.
Here is the solution, I figured-
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerBounds = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.bounds = outerLayerBounds // should be bounds not frame
let path = UIBezierPath(ovalIn: outerLayerBounds)
outerLayer.path = path.cgPath
outerLayer.position = CGPoint(x: outerLayerBounds.midX , y: outerLayerBounds.midY) //self.center doesn't center the layer, had to calculate the center of the layer manually
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}

Gradient from UIView after orientation transition

I have an extension for UIView to apply gradient:
extension UIView {
func applyGradient(colors: [CGColor]) {
self.backgroundColor = nil
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds // Here new gradientLayer should get actual UIView bounds
gradientLayer.cornerRadius = self.layer.cornerRadius
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.masksToBounds = true
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
In my UIView subclass I'm creating all my view and setting up constraints:
private let btnSignIn: UIButton = {
let btnSignIn = UIButton()
btnSignIn.setTitle("Sing In", for: .normal)
btnSignIn.titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
btnSignIn.layer.cornerRadius = 30
btnSignIn.clipsToBounds = true
btnSignIn.translatesAutoresizingMaskIntoConstraints = false
return btnSignIn
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSubViews()
}
func addSubViews() {
self.addSubview(imageView)
self.addSubview(btnSignIn)
self.addSubview(signUpstackView)
self.addSubview(textFieldsStackView)
setConstraints()
}
I've overridden layoutSubviews function which is called each time when view bounds are changed(Orientation transition included), where I'm calling applyGradient.
override func layoutSubviews() {
super.layoutSubviews()
btnSignIn.applyGradient(colors: [Colors.ViewTopGradient, Colors.ViewBottomGradient])
}
The problem is that after orientation transition gradient applied wrong for some reason...
See the screenshot please
What am I missing here?
If you look at your button, you’ll see two gradients. That’s because layoutSubviews is called at least twice, first when the view was first presented and again after the orientation change. So you’ve added at least two gradient layers.
You want to change this so you only insertSublayer once (e.g. while the view is being instantiated), and because layoutSubviews can be called multiple times, it should limit itself to just adjusting existing layers, not adding any.
You can also just use the layerClass class property to make the button’s main layer a gradient, and then you don’t have to manually adjust layer frames at all:
#IBDesignable
public class RoundedGradientButton: UIButton {
static public override var layerClass: AnyClass { CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
#IBInspectable var startColor: UIColor = .blue { didSet { updateColors() } }
#IBInspectable var endColor: UIColor = .red { didSet { updateColors() } }
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}
private extension RoundedGradientButton {
func configure() {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
updateColors()
titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
}
This technique eliminates the need to adjust the layer’s frame manually and results in better mid-animation renditions, too.

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

Custom UIButton background after orientation change

Learning Swift.
I have an UIButton class. Here is it:
class CustomBtn: UIButton {
var shadowLayer: CAShapeLayer!
var shadowAdded: Bool = false
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override func layoutSubviews() {
super.layoutSubviews()
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
super.drawRect(rect)
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
shadowLayer.fillColor = UIColor(netHex: Colors.Red1.value).CGColor
shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2
layer.insertSublayer(shadowLayer, atIndex: 0)
}
}
And this is my output:
But when I change orientation:
What would you do to get the button background (red color and the shadow) fill the width in any orientation?
You don't really need to override drawRect for this, so is best to heed the advice in the comment above it. But, the problem is you are setting your layers path based on your button's bounds, but those bounds change when you rotate the device, but you're not updating your layer.
Also many of the effects you're trying to achieve can be done by setting properties on your button's layer itself. I would change your class to this:
class CustomBtn: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame:CGRect){
super.init(frame: frame)
self.commonInit()
}
func commonInit(){
self.layer.backgroundColor = UIColor(netHex: Colors.Red1.value).CGColor
self.layer.cornerRadius = 5
self.layer.shadowColor = UIColor.darkGrayColor().CGColor
self.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
self.layer.shadowOpacity = 0.8
self.layer.shadowRadius = 2
}
}
You can get your class to be slightly more performant by adding to commonInit:
self.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
But if you do that you'll also want to add:
override var bounds: CGRect{
didSet{
self.commonInit()
}
}
So anytime the bounds change your shadow path will be updated.

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