Blink Arrow draw with UIBezierPath - ios

I have an UIScrollView which has a long text in it. I want to inform users that It has more content to read. Therefore, I added an arrow with UIBezierPath on bottom of it.
class ArrowView: UIView {
var arrowPath: UIBezierPath!
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
self.drawArrow(from: CGPoint(x: rect.midX, y: rect.minY), to: CGPoint(x: rect.midX, y: rect.maxY),
tailWidth: 10, headWidth: 25, headLength: 20)
//arrowPath.fill()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func drawArrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat){
let length = hypot(end.x - start.x, end.y - start.y)
let tailLength = length - headLength
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
let points: [CGPoint] = [
p(0, tailWidth / 2),
p(tailLength, tailWidth / 2),
p(tailLength, headWidth / 2),
p(length, 0),
p(tailLength, -headWidth / 2),
p(tailLength, -tailWidth / 2),
p(0, -tailWidth / 2)
]
let cosine = (end.x - start.x) / length
let sine = (end.y - start.y) / length
let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
let path = CGMutablePath()
path.addLines(between: points, transform: transform)
path.closeSubpath()
arrowPath = UIBezierPath.init(cgPath: path)
}
}
My question: How can I achieve a blink animation on arrow. Assume that It has a gradient layer from white to blue. It should start with white to blue then blue should seen on start point then white should start to seen on finish point of Arrow and this circle should continue.
On final, this animation should inform users that they can scroll the view.
How can I achieve this?

You can try using CAGradientLayer along with CAAnimation. Add gradient to your view:
override func viewDidAppear(animated: Bool) {
self.gradient = CAGradientLayer()
self.gradient?.frame = self.view.bounds
self.gradient?.colors = [ UIColor.white.cgColor, UIColor.white.cgColor]
self.view.layer.insertSublayer(self.gradient, atIndex: 0)
animateLayer()
}
func animateLayer(){
var fromColors = self.gradient.colors
var toColors: [AnyObject] = [UIColor.blue.cgColor,UIColor.blue.cgColor]
self.gradient.colors = toColors
var animation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
animation.fromValue = fromColors
animation.toValue = toColors
animation.duration = 3.00
animation.isRemovedOnCompletion = true
animation.fillMode = CAMediaTimingFillMode.forwards
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.delegate = self
self.gradient?.addAnimation(animation, forKey:"animateGradient")
}
and to continue gradient animation in cyclic order like:
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
self.toColors = self.fromColors;
self.fromColors = self.gradient?.colors
animateLayer()
}

You could put your arrow inside a UIView object. You could then have the UIView on a timer where it changed the alpha properly. Depending on blinking or fading, nonetheless, you could use UIView Animations to complete your desired animation.
I say a uiview, instead of using the scrollview because you don’t want to hide the scrollview. So you’d put it inside a view in which is only a container for the bezierpath.

I solved it by using CABasicAnimation(). This first add gradient layer to custom UIView then apply mask on it then add animation.
override func draw(_ rect: CGRect) {
// Drawing code
self.drawArrow(from: CGPoint(x: rect.midX, y: rect.minY), to: CGPoint(x: rect.midX, y: rect.maxY),
tailWidth: 10, headWidth: 20, headLength: 15)
//arrowPath.fill()
let startColor = UIColor(red:0.87, green:0.87, blue:0.87, alpha:1.0).cgColor
let finishColor = UIColor(red:0.54, green:0.54, blue:0.57, alpha:1.0).withAlphaComponent(0.5).cgColor
let gradient = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = [startColor,finishColor]
let shapeMask = CAShapeLayer()
shapeMask.path = arrowPath.cgPath
gradient.mask = shapeMask
let animation = CABasicAnimation(keyPath: "colors")
animation.fromValue = [startColor, finishColor]
animation.toValue = [finishColor, startColor]
animation.duration = 2.0
animation.autoreverses = true
animation.repeatCount = Float.infinity
//add the animation to the gradient
gradient.add(animation, forKey: nil)
self.layer.addSublayer(gradient)
}

Related

How to create a rotating rainbow color circle in iOS

From stackoverflow i got a code for drawing rainbow color circle.But as part of requirement ,I need that circle to be rotated continously ,like a rotating progress loader.Below is the code used for creating Rainbow color circle.
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
// //Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
Please anyone help me in rotating this circle.
Please find below the circle generated with above code:
What you got looks completely overcomplicated in the first place. Take a look at the following example:
class ViewController: UIViewController {
class RainbowView: UIView {
var segmentCount: Int = 10 {
didSet {
refresh()
}
}
var lineWidth: CGFloat = 10 {
didSet {
refresh()
}
}
override var frame: CGRect {
didSet {
refresh()
}
}
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
private var currentGradientLayer: CAGradientLayer?
private func refresh() {
currentGradientLayer?.removeFromSuperlayer()
guard segmentCount > 0 else { return }
currentGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.type = .conic
let colors: [UIColor] = {
var colors: [UIColor] = [UIColor]()
for i in 0..<segmentCount {
colors.append(UIColor(hue: CGFloat(i)/CGFloat(segmentCount), saturation: 1, brightness: 1, alpha: 1))
}
colors.append(UIColor(hue: 0.0, saturation: 1, brightness: 1, alpha: 1)) // Append start color at the end as well to complete the circle
return colors;
}()
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
gradientLayer.mask = {
let shapeLayer = CAShapeLayer()
shapeLayer.frame = bounds
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = UIBezierPath(ovalIn: bounds.inset(by: UIEdgeInsets(top: lineWidth*0.5, left: lineWidth*0.5, bottom: lineWidth*0.5, right: lineWidth*0.5))).cgPath
return shapeLayer
}()
return gradientLayer
}()
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview({
let view = RainbowView(frame: CGRect(x: 50.0, y: 100.0, width: 100.0, height: 100.0))
var angle: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { _ in
angle += 0.01
view.transform = CGAffineTransform(rotationAngle: angle)
})
return view
}())
}
}
So a view is generated that uses a conical gradient with mask to draw the circle you are describing. Then a transform is applied to the view to rotate it. And a Timer is scheduled to rotate the circle.
Note that this code will leak because timer is nowhere invalidated. It needs to be removed when view disappears or similar.
The easiest way would be to attach an animation that repeats forever:
let animation = CABasicAnimation(keyPath: "transform.rotation") // Create rotation animation
animation.repeatCount = .greatestFiniteMagnitude // Repeat animation for as long as we can
animation.fromValue = 0 // Rotate from 0
animation.toValue = 2 * Float.pi // to 360 deg
animation.duration = 1 // During 1 second
self.layer.add(animation, forKey: "animation") // Adding the animation to the view
self - is RainbowCircle, assuming that you add this code to one of the methods inside it.
For this we can have Image something like this
syncImage.image = UIImage(named:"spinning")
Create a below extension to Start/Stop Rotating
extension UIView {
// To animate
func startRotating(duration: Double = 1) {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) == nil {
let animate = CABasicAnimation(keyPath: "transform.rotation")
animate.duration = duration
animate.repeatCount = Float.infinity
animate.fromValue = 0.0
animate.toValue = Float(M_PI * 2.0)
self.layer.addAnimation(animate, forKey: kAnimationKey)
}
}
func stopRotating() {
let kAnimationKey = "rotation"
if self.layer.animationForKey(kAnimationKey) != nil {
self.layer.removeAnimationForKey(kAnimationKey)
}
}
}
Usage
func startSpinning() {
syncImage.startRotating()
}
func stopSpinning() {
syncImage.stopRotating()
}
func handleSyncTap(sender: UITapGestureRecognizer? = nil) {
startSpinning()
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.stopSpinning()
})
}

Dash the stroke of UIBezierPath without using the CAShapeLayer and animate this stroke

UIBezierPath only get dashed when used inside drawRect() method in UIView like so:
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
}
If I want to animate this line stroke, I'll be needing to use CAShapeLayer like so
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
layer.fillColor = UIColor.clear.cgColor
layer.lineJoin = kCALineCapButt
self.layer.addSublayer(layer)
animateStroke(layer: layer)
}
func animateStroke(layer:CAShapeLayer)
{
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 10
pathAnimation.fromValue = 0
pathAnimation.toValue = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
layer.add(pathAnimation, forKey: "strokeEnd")
}
The Black line of the CAShapeLayer got animated.
What I need is, to add dashed UIBezierpath to CAShapeLayer, so that I can animate it.
Note: I do not want to use CAShapeLayer's lineDashPattern method as I'm appending multiple paths some need to be dashed and some not.
You should not invoke animations from draw(_:). The draw(_:) is for rendering a single frame.
You say you don't want to use lineDashPattern, but I personally would, using a different shape layer for each pattern. So, for example, here is an animation, stroking one path with no dash pattern, stroking the other with dash pattern, and just triggering the second upon the completion of the first:
struct Stroke {
let start: CGPoint
let end: CGPoint
let lineDashPattern: [NSNumber]?
var length: CGFloat {
return hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private var strokeIndex = 0
private let strokeSpeed = 200.0
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
strokeIndex = 0
animateStroke()
}
private func animateStroke() {
guard let strokes = strokes, strokeIndex < strokes.count else { return }
let stroke = strokes[strokeIndex]
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineDashPattern = strokes[strokeIndex].lineDashPattern
shapeLayer.lineWidth = 8
shapeLayer.strokeColor = UIColor.red.cgColor
layer.addSublayer(shapeLayer)
let path = UIBezierPath()
path.move(to: stroke.start)
path.addLine(to: stroke.end)
shapeLayer.path = path.cgPath
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Double(stroke.length) / strokeSpeed
animation.delegate = self
shapeLayer.add(animation, forKey: nil)
}
}
extension CustomView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard flag else { return }
strokeIndex += 1
animateStroke()
}
}
If you really want to use the draw(_:) approach, you wouldn't use CABasicAnimation, but instead would probably use a CADisplayLink, repeatedly calling setNeedsDisplay(), and having a draw(_:) method that renders the view depending upon how much time has elapsed. But draw(_:) renders a single frame of the animation and should not initiate any CoreAnimation calls.
If you really don't want to use shape layers, you can use the aforementioned CADisplayLink to update the percent complete based upon the elapsed time and desired duration, and draw(_:) only strokes as many of the individual paths as appropriate for any given moment in time:
struct Stroke {
let start: CGPoint
let end: CGPoint
let length: CGFloat // in this case, because we're going call this a lot, let's make this stored property
let lineDashPattern: [CGFloat]?
init(start: CGPoint, end: CGPoint, lineDashPattern: [CGFloat]?) {
self.start = start
self.end = end
self.lineDashPattern = lineDashPattern
self.length = hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private let duration: CGFloat = 3.0
private var start: CFTimeInterval?
private var percentComplete: CGFloat?
private var totalLength: CGFloat?
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
totalLength = strokes?.reduce(0.0) { $0 + $1.length }
start = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
percentComplete = min(1.0, CGFloat(CACurrentMediaTime() - start!) / duration)
if percentComplete! >= 1.0 {
displayLink.invalidate()
percentComplete = 1
}
setNeedsDisplay()
}
// Note, no animation is in the following routine. This just stroke your series of paths
// until the total percent of the stroked path equals `percentComplete`. The animation is
// achieved above, by updating `percentComplete` and calling `setNeedsDisplay`. This method
// only draws a single frame of the animation.
override func draw(_ rect: CGRect) {
guard let totalLength = totalLength,
let strokes = strokes,
strokes.count > 0,
let percentComplete = percentComplete else { return }
UIColor.red.setStroke()
// Don't get lost in the weeds here; the idea is to simply stroke my paths until the
// percent of the lengths of all of the stroked paths reaches `percentComplete`. Modify
// the below code to match whatever model you use for all of your stroked paths.
var lengthSoFar: CGFloat = 0
var percentSoFar: CGFloat = 0
var strokeIndex = 0
while lengthSoFar / totalLength < percentComplete && strokeIndex < strokes.count {
let stroke = strokes[strokeIndex]
let endLength = lengthSoFar + stroke.length
let endPercent = endLength / totalLength
let percentOfThisStroke = (percentComplete - percentSoFar) / (endPercent - percentSoFar)
var end: CGPoint
if percentOfThisStroke < 1 {
let angle = atan2(stroke.end.y - stroke.start.y, stroke.end.x - stroke.start.x)
let distance = stroke.length * percentOfThisStroke
end = CGPoint(x: stroke.start.x + distance * cos(angle),
y: stroke.start.y + distance * sin(angle))
} else {
end = stroke.end
}
let path = UIBezierPath()
if let pattern = stroke.lineDashPattern {
path.setLineDash(pattern, count: pattern.count, phase: 0)
}
path.lineWidth = 8
path.lineCapStyle = .round
path.move(to: stroke.start)
path.addLine(to: end)
path.stroke()
strokeIndex += 1
lengthSoFar = endLength
percentSoFar = endPercent
}
}
}
This achieves the identical effect as the first code snippet, though likely it isn't going to be anywhere near as efficient.

How to make the background color property of a custom view animatable?

I want to animate my custom view's background color.
My drawing code is very simple. The draw(in:) method is overridden like this:
#IBDesignable
class SquareView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let strokeWidth = self.width / 8
let path = UIBezierPath()
path.move(to: CGPoint(x: self.width - strokeWidth / 2, y: 0))
path.addLine(to: CGPoint(x: self.width - strokeWidth / 2, y: self.height - strokeWidth / 2))
path.addLine(to: CGPoint(x: 0, y: self.height - strokeWidth / 2))
self.backgroundColor?.darker().setStroke()
path.lineWidth = strokeWidth
path.stroke()
}
}
The darker method just returns a darker version of the color it is called on.
When I set the background to blue, it draws something like this:
I want to animate its background color so that it gradually changes to a red color. This would be the end result:
I first tried:
UIView.animate(withDuration: 1) {
self.square.backgroundColor = .red
}
It just changes the color to red in an instant, without animation.
Then I researched and saw that I need to use CALayers. Therefore, I tried drawing the thing in layers:
#IBDesignable
class SquareViewLayers: UIView {
dynamic var squareColor: UIColor = .blue {
didSet {
setNeedsDisplay()
}
}
func setupView() {
layer.backgroundColor = squareColor.cgColor
let sublayer = CAShapeLayer()
let path = UIBezierPath()
let strokeWidth = self.width / 8
path.move(to: CGPoint(x: self.width - strokeWidth / 2, y: 0))
path.addLine(to: CGPoint(x: self.width - strokeWidth / 2, y: self.height - strokeWidth / 2))
path.addLine(to: CGPoint(x: 0, y: self.height - strokeWidth / 2))
self.tintColor.darker().setStroke()
path.lineWidth = strokeWidth
sublayer.path = path.cgPath
sublayer.strokeColor = squareColor.darker().cgColor
sublayer.lineWidth = strokeWidth
sublayer.backgroundColor = UIColor.clear.cgColor
layer.addSublayer(sublayer)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
}
But the result is horrible
I used this code to try to animate this:
let anim = CABasicAnimation(keyPath: "squareColor")
anim.fromValue = UIColor.blue
anim.toValue = UIColor.red
anim.duration = 1
square.layer.add(anim, forKey: nil)
Nothing happens though.
I am very confused. What is the correct way to do this?
EDIT:
The darker method is from a cocoa pod called SwiftyColor. This is how it is implemented:
// in an extension of UIColor
public func darker(amount: CGFloat = 0.25) -> UIColor {
return hueColor(withBrightnessAmount: 1 - amount)
}
private func hueColor(withBrightnessAmount amount: CGFloat) -> UIColor {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
return UIColor(hue: hue, saturation: saturation,
brightness: brightness * amount,
alpha: alpha)
}
return self
}
You're on the right track with CABasicAnimation.
The keyPath: in let anim = CABasicAnimation(keyPath: "squareColor") should be backgroundColor.
anim.fromValue and anim.toValue require CGColor values (because you're operating on the CALayer, which uses CGColor). You can use UIColor.blue.cgcolor here.
Pretty sure this animation won't persist the color change for you though, so you might need to change that property manually.
Could you stroke the L-shape with a semi-transparent black instead?
Then you don't have to mess around with redrawing and color calculations, and you can use the basic UIView.animate()
Below the "LShadow" is added onto the SquareView, so you can set the SquareView background color as per usual with UIView.animate()
ViewController.swift
import UIKit
class ViewController: UIViewController {
var squareView: SquareView!
override func viewDidLoad() {
super.viewDidLoad()
squareView = SquareView.init(frame: CGRect(x:100, y:100, width:200, height:200))
self.view.addSubview(squareView)
squareView.backgroundColor = UIColor.blue
UIView.animate(withDuration: 2.0, delay: 1.0, animations: {
self.squareView.backgroundColor = UIColor.red
}, completion:nil)
}
}
SquareView.swift
import UIKit
class SquareView: UIView {
func setupView() {
let shadow = LShadow.init(frame: self.bounds)
self.addSubview(shadow)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
}
class LShadow: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
let strokeWidth = self.frame.size.width / 8
let path = UIBezierPath()
path.move(to: CGPoint(x: self.frame.size.width - strokeWidth / 2, y: 0))
path.addLine(to: CGPoint(x: self.frame.size.width - strokeWidth / 2, y: self.frame.size.height - strokeWidth / 2))
path.addLine(to: CGPoint(x: 0, y: self.frame.size.height - strokeWidth / 2))
UIColor.init(white: 0, alpha: 0.5).setStroke()
path.lineWidth = strokeWidth
path.stroke()
}
}
Using CAKeyframeAnimation you need to animate backgroundColor property:
class SquareView: UIView
{
let sublayer = CAShapeLayer()
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
let frame = self.frame
let strokeWidth = self.frame.width / 8
sublayer.frame = self.bounds
let path = UIBezierPath()
path.move(to: CGPoint(x: frame.width - strokeWidth / 2 , y: 0))
path.addLine(to: CGPoint(x: frame.width - strokeWidth / 2, y: frame.height - strokeWidth / 2))
path.addLine(to: CGPoint(x: 0, y: frame.height - strokeWidth / 2))
path.addLine(to: CGPoint(x: 0 , y: 0))
path.lineWidth = strokeWidth
sublayer.path = path.cgPath
sublayer.fillColor = UIColor.blue.cgColor
sublayer.lineWidth = strokeWidth
sublayer.backgroundColor = UIColor.blue.cgColor
layer.addSublayer(sublayer)
}
//Action
func animateIt()
{
let animateToColor = UIColor(cgColor: sublayer.backgroundColor!).darker().cgColor
animateColorChange(mLayer: self.sublayer, colors: [sublayer.backgroundColor!, animateToColor], duration: 1)
}
func animateColorChange(mLayer:CALayer ,colors:[CGColor], duration:CFTimeInterval)
{
let animation = CAKeyframeAnimation(keyPath: "backgroundColor")
CATransaction.begin()
animation.keyTimes = [0.2, 0.5, 0.7, 1]
animation.values = colors
animation.calculationMode = kCAAnimationPaced
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.repeatCount = 0
animation.duration = duration
mLayer.add(animation, forKey: nil)
CATransaction.commit()
}
}
Note: I have edited the answer so that the shape will always fit the parent view added from storyboard
I had an implementation of changing the background color of view with animation and did some tweak in the source code to add inverted L shape using CAShapeLayer. I achieved the required animation. here is the sample of the view.
class SquareView: UIView, CAAnimationDelegate {
fileprivate func setup() {
self.layer.addSublayer(self.sublayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup
}
let sublayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let strokeWidth = self.bounds.width / 8
var frame = self.bounds
frame.size.width -= strokeWidth
frame.size.height -= strokeWidth
self.sublayer.path = UIBezierPath(rect: frame).cgPath
}
var color : UIColor = UIColor.clear {
willSet {
self.sublayer.fillColor = newValue.cgColor
self.backgroundColor = newValue.darker()
}
}
func animation(color: UIColor, duration: CFTimeInterval = 1.0) {
let animation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.fillMode = kCAFillModeForwards
animation.duration = duration
animation.repeatCount = 1
animation.autoreverses = false
animation.fromValue = self.backgroundColor?.cgColor
animation.toValue = color.darker().cgColor
animation.delegate = self;
let shapeAnimation = CABasicAnimation()
shapeAnimation.keyPath = "fillColor"
shapeAnimation.fillMode = kCAFillModeForwards
shapeAnimation.duration = duration
shapeAnimation.repeatCount = 1
shapeAnimation.autoreverses = false
shapeAnimation.fromValue = self.sublayer.fillColor
shapeAnimation.toValue = color.cgColor
shapeAnimation.delegate = self;
self.layer.add(animation, forKey: "kAnimation")
self.sublayer.add(shapeAnimation, forKey: "kAnimation")
self.color = color
}
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.layer.removeAnimation(forKey: "kAnimation")
self.sublayer.removeAnimation(forKey: "kAnimation")
}
}
You can use the method animation(color: duration:) to change the color with animation on required animation duration.
Example
let view = SquareView(frame: CGRect(x: 10, y: 100, width: 200, height: 300))
view.color = UIColor.blue
view.animation(color: UIColor.red)
Here is the result
We have in the code below, two different CAShapeLayer, foreground and background. Background is a rectangle path drawn to the extent where inverted L actually starts. And L is the path created with simple UIBezierPath geometry. I animate the change to fillColor for each layer after color is set.
class SquareViewLayers: UIView, CAAnimationDelegate {
var color: UIColor = UIColor.blue {
didSet {
animate(layer: backgroundLayer,
from: oldValue,
to: color)
animate(layer: foregroundLayer,
from: oldValue.darker(),
to: color.darker())
}
}
let backgroundLayer = CAShapeLayer()
let foregroundLayer = CAShapeLayer()
func setupView() {
foregroundLayer.frame = frame
layer.addSublayer(foregroundLayer)
backgroundLayer.frame = frame
layer.addSublayer(backgroundLayer)
let strokeWidth = width / 8
let backgroundPathRect = CGRect(origin: .zero, size: CGSize(width: width - strokeWidth,
height: height - strokeWidth))
let backgroundPath = UIBezierPath(rect: backgroundPathRect)
backgroundLayer.fillColor = color.cgColor
backgroundLayer.path = backgroundPath.cgPath
let foregroundPath = UIBezierPath()
foregroundPath.move(to: CGPoint(x: backgroundPathRect.width,
y: 0))
foregroundPath.addLine(to: CGPoint(x: width,
y: 0))
foregroundPath.addLine(to: CGPoint(x: width, y: height))
foregroundPath.addLine(to: CGPoint(x: 0,
y: height))
foregroundPath.addLine(to: CGPoint(x: 0,
y: backgroundPathRect.height))
foregroundPath.addLine(to: CGPoint(x: backgroundPathRect.width,
y: backgroundPathRect.height))
foregroundPath.addLine(to: CGPoint(x: backgroundPathRect.width,
y: 0))
foregroundPath.close()
foregroundLayer.path = foregroundPath.cgPath
foregroundLayer.fillColor = color.darker().cgColor
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func animate(layer: CAShapeLayer,
from: UIColor,
to: UIColor) {
let fillColorAnimation = CABasicAnimation(keyPath: "fillColor")
fillColorAnimation.fromValue = from.cgColor
layer.fillColor = to.cgColor
fillColorAnimation.duration = 1.0
layer.add(fillColorAnimation,
forKey: nil)
}
}
And here is the result,
Objective-C
view.backgroundColor = [UIColor whiteColor].CGColor;
[UIView animateWithDuration:2.0 animations:^{
view.layer.backgroundColor = [UIColor greenColor].CGColor;
} completion:NULL];
Swift
view.backgroundColor = UIColor.white.cgColor
UIView.animate(withDuration: 2.0) {
view.backgroundColor = UIColor.green.cgColor
}

Animating a circular progress circle smoothly via buttons

I am experiencing odd visual quirks with my progress circle. I would like the green circle (CAShapeLayer) to smoothly animate in increments of fifths. 0/5 = no green circle. 5/5 = full green circle. The problem is the green circle is animating past where it should be, then abruptly shrinks back to the proper spot. This happens when both the plus and minus button are pressed. There is a gif below demonstrating what is happening.
The duration of each animation should last 0.25 seconds, and should smoothly animate both UP and DOWN (depending if the plus or minus button is pressed) from wherever the animation ended last (which is currently not happening.)
In my UIView, I draw the circle, along with the method to animate the position of the progress:
class CircleView: UIView {
let progressCircle = CAShapeLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func draw(_ rect: CGRect) {
let circlePath = UIBezierPath(ovalIn: self.bounds)
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.green.cgColor
progressCircle.fillColor = UIColor.red.cgColor
progressCircle.lineWidth = 10.0
// Add the circle to the view.
self.layer.addSublayer(progressCircle)
}
func animateCircle(circleToValue: CGFloat) {
let fifths:CGFloat = circleToValue / 5
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 0.25
animation.fromValue = fifths
animation.byValue = fifths
animation.fillMode = kCAFillModeBoth
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
progressCircle.strokeEnd = fifths
// Create the animation.
progressCircle.add(animation, forKey: "strokeEnd")
}
}
In my VC:
I init the circle and starting point with:
let myDrawnCircle = CircleView()
var startingPointForCircle = CGFloat()
viewDidAppear:
startingPointForCircle = 0.0
myDrawnCircle.frame = CGRect(x: 100, y: 100, width: 150, height: 150)
myDrawnCircle.backgroundColor = UIColor.white
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
self.view.addSubview(myDrawnCircle)
textLabel.text = "\(startingPointForCircle)"
And the actual buttons that do the cool animating:
#IBAction func minusButtonPressed(_ sender: UIButton) {
if startingPointForCircle > 0 {
startingPointForCircle -= 1
textLabel.text = "\(startingPointForCircle)"
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
}
}
#IBAction func plusButtonPressed(_ sender: UIButton) {
if startingPointForCircle < 5 {
startingPointForCircle += 1
textLabel.text = "\(startingPointForCircle)"
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
}
}
Here's a gif showing you what's going on with the jerky animations.
How do I make my animations smoothly animate TO the proper value FROM the proper value?
My guess is it has something to do with using a key path. I don't know how exactly to fix it for using a key path, but you could try a different tack, using a custom view, and animating the value passed to the method used for drawing an arc.
The code below was generated by PaintCode. The value passed for arc specifies the point on the circle to draw the arc to. This is used in the drawRect method of a custom UIView. Make a CGFloat property of the custom view and simply change that value, and call setNeedsDisplay on the view after your animation code.
func drawCircleProgress(frame frame: CGRect = CGRect(x: 0, y: 0, width: 40, height: 40), arc: CGFloat = 270) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Color Declarations
let white = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
let white50 = white.colorWithAlpha(0.5)
//// Oval Drawing
CGContextSaveGState(context)
CGContextTranslateCTM(context, frame.minX + 20, frame.minY + 20)
CGContextRotateCTM(context, -90 * CGFloat(M_PI) / 180)
let ovalRect = CGRect(x: -19.5, y: -19.5, width: 39, height: 39)
let ovalPath = UIBezierPath()
ovalPath.addArcWithCenter(CGPoint(x: ovalRect.midX, y: ovalRect.midY), radius: ovalRect.width / 2, startAngle: 0 * CGFloat(M_PI)/180, endAngle: -arc * CGFloat(M_PI)/180, clockwise: true)
white.setStroke()
ovalPath.lineWidth = 1
ovalPath.stroke()
CGContextRestoreGState(context)
}
The desired results were as simple as commenting out fromValue / byValue in the animateCircle method. This was pointed out by #dfri - "set both fromValue and toValue to nil then: "Interpolates between the previous value of keyPath in the target layer’s presentation layer and the current value of keyPath in the target layer’s presentation layer."
//animation.fromValue = fifths
//animation.byValue = fifths

Animate CAShapeLayer & Shadow independently

I have a 'UIView' Subclass called 'HexagonView' in which I create a layer to display a Hexagon. I then add another layer with the same path as shadow layer. I had to do it this way since I could not display a shadow outside of the 'layer.mask' Shape.
My goal is to animate a 360° rotation of the shape, while the shadow stays on it's position instead of moving around with the rotation.
See example image
This is the code I have so far:
#IBDesignable class HexagonView: UIView {
override func prepareForInterfaceBuilder()
{
super.prepareForInterfaceBuilder()
configureLayer()
addShadow()
}
override init(frame: CGRect)
{
super.init(frame: frame)
configureLayer()
addShadow()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
configureLayer()
addShadow()
}
func configureLayer()
{
let maskLayer = CAShapeLayer()
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.frame = bounds
UIGraphicsBeginImageContext(frame.size)
maskLayer.path = getHexagonPath().cgPath
UIGraphicsEndImageContext()
layer.addSublayer(maskLayer)
}
private func getHexagonPath() -> UIBezierPath
{
let width = frame.size.width
let height = frame.size.height
let padding = width * 1 / 8 / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: width / 2, y: 0))
path.addLine(to: CGPoint(x: width - padding, y: height / 4))
path.addLine(to: CGPoint(x: width - padding, y: height * 3 / 4))
path.addLine(to: CGPoint(x: width / 2, y: height))
path.addLine(to: CGPoint(x: padding, y: height * 3 / 4))
path.addLine(to: CGPoint(x: padding, y: height / 4))
path.close()
return path
}
func addShadow()
{
let s = CAShapeLayer()
s.fillColor = backgroundColor?.cgColor
s.frame = layer.bounds
s.path = getHexagonPath().cgPath
layer.backgroundColor = UIColor.clear.cgColor
layer.addSublayer(s)
layer.masksToBounds = false
layer.shadowColor = AppAppearance.Colors.labelBackground.cgColor
layer.shadowOffset = CGSize(width: 5, height: 10)
layer.shadowOpacity = 0.5
layer.shadowPath = getHexagonPath().cgPath
layer.shadowRadius = 0
}
func rotate(duration: CFTimeInterval = 1.0, completionDelegate: AnyObject? = nil)
{
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
if let delegate = completionDelegate as? CAAnimationDelegate {
rotateAnimation.delegate = delegate
}
layer.add(rotateAnimation, forKey: nil)
}
}
How can I achieve the shadow rotation on its starting position rather than having it attached to the white layer.

Resources