I wrote a Behind the scene of delightful animation it is about Animation patterns in modern iOS Apps. In this post, I’ll introduce how to animate path using CoreAnimation.

Before we start coding, we need to prepare the vector image like an SVG and then convert it to UIBeizierpath.

 

SVG Image

SVG acronym is Scalable Vector Graphics developed by W3C. Unfortunately, iOS does not support the SVG format. So We need to convert it to UIBeizierpath.

#block-yui_3_17_2_1_1589705258661_10561 .sqs-gallery-block-grid .sqs-gallery-design-grid { margin-right: -0px; }
#block-yui_3_17_2_1_1589705258661_10561 .sqs-gallery-block-grid .sqs-gallery-design-grid-slide .margin-wrapper { margin-right: 0px; margin-bottom: 0px; }

Look at path tag. Each character has the meaning of a drawing command. Uppercase uses absolute position, and lowercase uses relative position.

  • M = moveto

  • L = lineto

  • H = horizontal lineto

  • V = vertical lineto

  • C = curveto

  • S = smooth curveto

  • Q = quadratic Bézier curve

  • T = smooth quadratic Bézier curveto

  • A = elliptical Arc

  • Z = closepath

We can convert it to UIBeizierPath.

  • move(to: CGPoint)

  • addCurve(to: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)

  • addQuadCurve(to: CGPoint, controlPoint: CGPoint)

  • addLine(to: CGPoint)

  • addArc(withCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockWise: Bool)

 

How to convert SVG to UIBeizierPath?

Here is an example of Swift Logo.

carbonDrawing.JPG

Let’s convert it to UIBeizierPath step by step.

<path d="m29.885 33.047c-4.667 2.696-11.084 2.973-17.54 0.206-5.2273-2.224-9.5646-6.117-12.345-10.565 1.3346 1.112 2.8916 2.002 4.5598 2.78 6.6672 3.125 13.333 2.911 18.024 0.008-0.003-0.003-0.005-0.005-0.007-0.008-6.673-5.116-12.345-11.789-16.571-17.238-0.8901-0.8898-1.5574-2.002-2.2247-3.0029 5.1159 4.671 13.235 10.565 16.126 12.234-6.116-6.451-11.566-14.458-11.344-14.236 9.676 9.787 18.685 15.348 18.685 15.348 0.298 0.168 0.528 0.308 0.713 0.433 0.195-0.496 0.366-1.011 0.51-1.545 1.557-5.672-0.222-12.123-4.115-17.461 9.008 5.4495 14.347 15.681 12.122 24.245-0.058 0.231-0.121 0.459-0.189 0.683 0.026 0.031 0.052 0.063 0.078 0.096 4.448 5.561 3.225 11.455 2.669 10.343-2.413-4.722-6.88-3.278-9.151-2.32z"/> 

StartPoint is 29.88, 33.05

StartPoint is 29.88, 33.05

//First drawing command
//m29.885 33.047
move(to: CGPoint(x: 29.885, y: 33.05)

The m command is easy. Just move the point using move(to: CGPoint).

Second point is 12.34, 33.25. It is calculated by startPoint x: 29.885 - 17.54 = 12.35 and startPoint: y: 33.05 + 0.206 = 33.25.

Second point is 12.34, 33.25. It is calculated by startPoint x: 29.885 – 17.54 = 12.35 and startPoint: y: 33.05 + 0.206 = 33.25.

//Second drawing command
//c-4.667 2.696-11.084 2.973-17.54 0.206
addCurve(
to: CGPoint(x: 12.35, y: 33.25), 
controlPoint1: CGPoint(x: 25.22, y: 35.74), 
controlPoint2: CGPoint(x: 18.8, y: 36.02)
)

Look at the c character. The c is meaning that curveTo is a relative position. It takes 3 points, which are controlPoint1, controlPoint2, and currentPoint. I was very confused that how can it be converted from (-4.667, 2.696), (-11.084, 2.973), and (-17.54, 0.206) to (12.35, 33.25), (25.22, 35.74), and (18.8, 36.02).

Let’s look again. We take 3 points, which are controlPoint1, controlPoint2, and current Position.

Let’s look again. We take 3 points, which are controlPoint1, controlPoint2, and current Position.

controlPoint2.JPG

controlPoint1.JPG

The startPosition is CGPoint(x: 29.885, y: 33.05). And It was added curveTo relative to startPosition.

  • CurrentPosition(x: 12.35, y: 33.25) is calculated by 29.885 – 17.54 = 12.35 and 33.05 + 0.206 = 33.25

  • ControlPoint1(x: 25.22, y: 35.74) is calculated by 29.885 – 4.667 = 25.22 and 33.05 + 2.697 = 35.74

  • ControlPoint2(x: 18.8, y: 36.02) is calculated by 29.885 – 11.084 = 18.8 and 33.05 + 2.973 = 36.02

And next position is also same. Just take a 3 position and calculate it relative to currentPosition(x: 12.35, y: 33.25) which is updated after add curveTo position. Here is the full path of Swift Logo.

extension UIBezierPath {
static var swift: UIBezierPath = {
    let bezierPath = UIBezierPath()
    bezierPath.move(to: CGPoint(x: 29.88, y: 33.05))
    bezierPath.addCurve(to: CGPoint(x: 12.35, y: 33.25), controlPoint1: CGPoint(x: 25.22, y: 35.74), controlPoint2: CGPoint(x: 18.8, y: 36.02))
    bezierPath.addCurve(to: CGPoint(x: 0, y: 22.69), controlPoint1: CGPoint(x: 7.12, y: 31.03), controlPoint2: CGPoint(x: 2.78, y: 27.14))
    bezierPath.addCurve(to: CGPoint(x: 4.56, y: 25.47), controlPoint1: CGPoint(x: 1.33, y: 23.8), controlPoint2: CGPoint(x: 2.89, y: 24.69))
    bezierPath.addCurve(to: CGPoint(x: 22.58, y: 25.48), controlPoint1: CGPoint(x: 11.23, y: 28.59), controlPoint2: CGPoint(x: 17.89, y: 28.38))
    bezierPath.addCurve(to: CGPoint(x: 22.58, y: 25.47), controlPoint1: CGPoint(x: 22.58, y: 25.47), controlPoint2: CGPoint(x: 22.58, y: 25.47))
    bezierPath.addCurve(to: CGPoint(x: 6.01, y: 8.23), controlPoint1: CGPoint(x: 15.9, y: 20.35), controlPoint2: CGPoint(x: 10.23, y: 13.68))
    bezierPath.addCurve(to: CGPoint(x: 3.78, y: 5.23), controlPoint1: CGPoint(x: 5.12, y: 7.34), controlPoint2: CGPoint(x: 4.45, y: 6.23))
    bezierPath.addCurve(to: CGPoint(x: 19.91, y: 17.46), controlPoint1: CGPoint(x: 8.9, y: 9.9), controlPoint2: CGPoint(x: 17.02, y: 15.79))
    bezierPath.addCurve(to: CGPoint(x: 8.56, y: 3.23), controlPoint1: CGPoint(x: 13.79, y: 11.01), controlPoint2: CGPoint(x: 8.34, y: 3))
    bezierPath.addCurve(to: CGPoint(x: 27.25, y: 18.57), controlPoint1: CGPoint(x: 18.24, y: 13.01), controlPoint2: CGPoint(x: 27.25, y: 18.57))
    bezierPath.addCurve(to: CGPoint(x: 27.96, y: 19.01), controlPoint1: CGPoint(x: 27.55, y: 18.74), controlPoint2: CGPoint(x: 27.78, y: 18.88))
    bezierPath.addCurve(to: CGPoint(x: 28.47, y: 17.46), controlPoint1: CGPoint(x: 28.16, y: 18.51), controlPoint2: CGPoint(x: 28.33, y: 18))
    bezierPath.addCurve(to: CGPoint(x: 24.36, y: 0), controlPoint1: CGPoint(x: 30.03, y: 11.79), controlPoint2: CGPoint(x: 28.25, y: 5.34))
    bezierPath.addCurve(to: CGPoint(x: 36.48, y: 24.25), controlPoint1: CGPoint(x: 33.36, y: 5.45), controlPoint2: CGPoint(x: 38.7, y: 15.68))
    bezierPath.addCurve(to: CGPoint(x: 36.29, y: 24.93), controlPoint1: CGPoint(x: 36.42, y: 24.48), controlPoint2: CGPoint(x: 36.36, y: 24.7))
    bezierPath.addCurve(to: CGPoint(x: 36.37, y: 25.02), controlPoint1: CGPoint(x: 36.32, y: 24.96), controlPoint2: CGPoint(x: 36.34, y: 24.99))
    bezierPath.addCurve(to: CGPoint(x: 39.04, y: 35.37), controlPoint1: CGPoint(x: 40.82, y: 30.59), controlPoint2: CGPoint(x: 39.59, y: 36.48))
    bezierPath.addCurve(to: CGPoint(x: 29.88, y: 33.05), controlPoint1: CGPoint(x: 36.62, y: 30.65), controlPoint2: CGPoint(x: 32.16, y: 32.09))
    bezierPath.close()
    return bezierPath
}()
}

 

Let’s animating it.

Path Animation

PathAnimation

PathAnimation

class ViewController: UIViewController {
    var swiftPath: UIBezierPath = .swift
    lazy var logoLayer: CAShapeLayer = {
       let logoLayer = CAShapeLayer()
        logoLayer.path = swiftPath.cgPath
        logoLayer.strokeEnd = 0
        logoLayer.strokeStart = 0
        logoLayer.lineWidth = 2

        logoLayer.borderColor = UIColor.black.cgColor
        logoLayer.strokeColor = UIColor.black.cgColor
        logoLayer.fillColor = UIColor.white.cgColor

        logoLayer.position = CGPoint(x: 161, y: 247)
        return logoLayer
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.layer.addSublayer(logoLayer)
        startPathAnimation()
    }

    func startPathAnimation() {
        let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
        pathAnimation.fromValue = 0
        pathAnimation.toValue = 1
        pathAnimation.duration = 2
        pathAnimation.isRemovedOnCompletion = false
        pathAnimation.fillMode = .forwards
        logoLayer.add(pathAnimation, forKey: "line")
    }
}

 

Fill Color Animation

fillColor.gif

func startFillColorAnimation() {
  let fillColorAnimation: CABasicAnimation = CABasicAnimation(
  keyPath: "fillColor"
  )
  fillColorAnimation.duration = 1
  //Start Fill Color Animation after finishing path animation.
  fillColorAnimation.beginTime = CACurrentMediaTime() + 2
  fillColorAnimation.fromValue = UIColor.clear.cgColor
  fillColorAnimation.toValue = UIColor.red.cgColor
  fillColorAnimation.fillMode = .forwards
  //Keep color after complete animation
  fillColorAnimation.isRemovedOnCompletion = false
  logoLayer.add(fillColorAnimation, forKey: "fill")
 }

 

Fill Gradient Color Animation

Unlike SVG, CAShapeLayer doen’t support gradient color. So We should use CAGradientLayer to fill the gradient color in Swift logo. I’ll show you how to change the gradient color from original orange gradient color of Swift logo to blue gradient color of SwiftUI logo.

let swiftUIColor: [CGColor] = [
  UIColor(red: 0/255, green: 240/255, blue: 245/255, alpha: 1).cgColor,
  UIColor(red: 0/255, green: 5/255, blue: 140/255, alpha: 1).cgColor
]

let swiftColor: [CGColor] = [
UIColor(red: 248/255, green: 138/255, blue: 54/255, alpha: 1).cgColor,
UIColor(red: 253/255, green: 32/255, blue: 32/255, alpha: 1).cgColor
]
override func viewDidLoad() {
    super.viewDidLoad()
    view.layer.addSublayer(logoLayer)

    startPathAnimation()
    startFillColorAnimation()
    fillGradientColor(colors: swiftColor)
}

func fillGradientColor(colors: [CGColor]) {
  let gradient = CAGradientLayer(layer: logoLayer)
  gradient.frame = swiftPath.bounds
  gradient.colors = colors
  logoLayer.addSublayer(gradient)
}

🤪 CAGradientLayer cover the CAShapeLayer.

🤪 CAGradientLayer cover the CAShapeLayer.

To fix CAGradientLayer cover the Swift logo, We need to clip mask before addSublayer.

func fillGradientColor(colors: [CGColor]) {
  let gradient = CAGradientLayer(layer: logoLayer)
  gradient.frame = swiftPath.bounds
  gradient.colors = colors
  //Shape Mask!
  let shapeMask = CAShapeLayer()
  shapeMask.path = swiftPath.cgPath
  gradient.mask = shapeMask
  logoLayer.addSublayer(gradient)
}

Tada~ 😎 Looks great!

Tada~ 😎 Looks great!

Ok. Almost done! Let’s fill gradient color.

override func viewDidLoad() {
  super.viewDidLoad()
  view.layer.addSublayer(logoLayer)

  startPathAnimation()
  startFillColorAnimation()
  fillGradientColor(colors: [UIColor.clear.cgColor, UIColor.clear.cgColor])
}

func startPathAnimation() {
  let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
  pathAnimation.fromValue = 0
  pathAnimation.toValue = 1
  pathAnimation.duration = 2
  pathAnimation.isRemovedOnCompletion = false
  pathAnimation.fillMode = .forwards
  logoLayer.add(pathAnimation, forKey: "line")
}

func startFillColorAnimation() {
  let fillColorAnimation: CABasicAnimation = CABasicAnimation(keyPath: "fillColor")
  fillColorAnimation.duration = 1
  fillColorAnimation.beginTime = CACurrentMediaTime() + 2
  fillColorAnimation.fromValue = UIColor.clear.cgColor
  fillColorAnimation.toValue = UIColor.red.cgColor
  fillColorAnimation.fillMode = .forwards
  fillColorAnimation.isRemovedOnCompletion = false
  logoLayer.add(fillColorAnimation, forKey: "fill")
}

func fillGradientColor(colors: [CGColor]) {
  let gradient = CAGradientLayer(layer: logoLayer)
  gradient.frame = swiftPath.bounds
  gradient.colors = colors
  //Shape Mask!
  let shapeMask = CAShapeLayer()
  shapeMask.path = swiftPath.cgPath
  gradient.mask = shapeMask
  logoLayer.addSublayer(gradient)

  //Start Animation
  startGradientAnimation(gradientLayer: gradient)
}

func startGradientAnimation(gradientLayer: CAGradientLayer) {
  let colorAnimation : CABasicAnimation = CABasicAnimation(keyPath: "colors")
  colorAnimation.duration = 2
  colorAnimation.beginTime = CACurrentMediaTime() + 3
  colorAnimation.fromValue = swiftColor
  colorAnimation.toValue = swiftUIColor
  colorAnimation.isRemovedOnCompletion = false
  colorAnimation.fillMode = .forwards
  gradientLayer.add(colorAnimation, forKey: "colors")
}

All done!

All done!

Thank you for reading my article. If you find any awkward expressions, I would appreciate your advice.

 

I want to say that Thank you for review this post!

Divjjot Singh helps me to improve my English writing skills. Thank you!

Quote of the week

"People ask me what I do in the winter when there's no baseball. I'll tell you what I do. I stare out the window and wait for spring."

~ Rogers Hornsby