Tag: CoreAnimation

  • How to Animate Path Using CoreAnimation in iOS – SVG to UIBezierPath

    How to Animate Path Using CoreAnimation in iOS – SVG to UIBezierPath

    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!

  • 4 Modern iOS Animation Patterns – Launch, Loading, Transition, and Interruptible

    4 Modern iOS Animation Patterns – Launch, Loading, Transition, and Interruptible

    I had presented about iOS animations at LetSwift conference last year. I classified the animations by researching the modern iOS app. There are 4 modern animation patterns.

    • Launch Animation

    • Loading Animation

    • View Transition

    • Interruptible Animation

    Before look at the animation patterns, Let’s take a look principle of animation. This principle is helping you when to decide the easing functions on your animation code.

    The principle of the animation

    The Illusion of Life: Disney Animation

    The Illusion of Life: Disney Animation

    In 1981, Frank Thomas and Ollie Johnston define the twelve principles of animations. I select the 4 principles which can reflect UI animation. These principles affect the easing functions in the Animation framework.

    The core animation framework has an easing function. We can set the easeIn, easeInOut, easeOut, and linear with duration time. Keep in mind these principles, I’ll show the examples and guides about easing function.

    This principle can reflect the launch animation, view transition and morphing icons.

    The secondary action principle reflects the physics effect, keyFrame animation, and interruptible animation.

    Easing functions

    easing.png

    Robert Penner is the creator of the easing function. It is widely used in various programming languages. The easing functions are basically inspired by twelve principles of animations. Let’s look at which easing function are suitable for UI.

    uber app

    uber app

    Look at Side Menu. The speed of showing and hiding the menu is different. Isn’t it?
    Here is my guide about the easing function.

    • Use ease-out when the view will appear.

      • duration 150ms – 350ms

    • Use ease-in or linear when the view will disappear.

      • duration 100ms – 150ms

     

    Modern iOS Animation

    Launch Animation

    The launch animation is the first impression and gives a delightful experience to the user. Technically, an iOS app is fetching data from the server, and It takes time to show the interface. So the fancy iOS apps use the launch animations while loading data.

    twitter.gif

    nike.gif

    uber.gif

    launch.gif

    I make the launch animation. I’ll post about how to make your launch animation.

    Loading Animation

    The loading animation displayed on the placeholder does not cover the screen, so it looks much cleaner.

    fbLoading.gif

    instagram.gif

    left_right_loading.gif

    I make the loading animation. I’ll post about how to make your loading animation.

    View Transition

    ViewTransition gives seamless experience compare to push view controller and present view controller, which is the default iOS presenting style. Technically you can implement your view transition style using UIViewControllerAnimatedTransitioning. I’ll post about UIViewControllerAnimatedTransitioning.

    zenly.gif

    nike.gif

    pinterest.gif

    instagram.gif

    Interruptible Animation

    You can see how Interruptible Animation works on the Apple Maps app. When you swipe up and down, then It will be starting Animation to expand the content. Interruptible Animation allows canceling while animating. Apple introduces this feature on iOS 10, and it called UIPropertyAnimator. I’ll also post about it soon.

    applemap.gif

     

    Conclusion

    I introduce the modern iOS animation styles. There are four kinds of patterns, and I’ll post how to implement these four patterns using CoreAnimation. If you want to get more information, then please look at references.

    References

    Google Fundamentals Design and UX

    Apple Core Animation Guide

    Disney 12 Principle Animations

    https://easings.net

    Understand-the-12-principles-of-animation

    Books

    Raywenderlich iOS Animation Book

    iOS Core Animation: Advanced Techniques

    Youtube

    Coding Math

    WWDC

    2017 Advances in UIKit Animations and Transitions