1

How to make snake like border animation in swift iOS

enter image description here

With the blow code, its not continues animation. once it completes its starting from another location. Note: I need to support corner radius for rectangle.

Current code:

    borderShapeLayer.path = UIBezierPath(roundedRect: contentView.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 15, height: 15)).cgPath


    borderShapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
    borderShapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
    borderShapeLayer.lineWidth = 5
    borderShapeLayer.strokeStart = 0.8

    let startAnimation = CABasicAnimation(keyPath: "strokeStart")
    startAnimation.fromValue = 0
    startAnimation.toValue = 0.8

    let endAnimation = CABasicAnimation(keyPath: "strokeEnd")
    endAnimation.fromValue = 0.2
    endAnimation.toValue = 1.0

    let animation = CAAnimationGroup()
    animation.animations = [startAnimation, endAnimation]
    animation.duration = 20
    animation.repeatCount = .infinity
    borderShapeLayer.add(animation, forKey: "MyAnimation")
    
    contentView.layer.addSublayer(borderShapeLayer)
Saranjith
  • 11,242
  • 5
  • 69
  • 122

2 Answers2

0

I needed to implement a similar solution and came up with this.

import UIKit

class RoundedRectActivityIndicator: UIView {
    private var shapeLayer: CAShapeLayer!
    private var progressLayer1: CAShapeLayer!
    private var progressLayer2: CAShapeLayer!
    
    let cornerRadius: CGFloat
    let lineWidth: CGFloat
    private(set) var segmentLength: CGFloat
    let lineCap: CAShapeLayerLineCap
    
    private var readyForDrawing = false
    private(set) var isAnimating = false
    
    private var strokeColor: UIColor
    private var strokeBackgroundColor: UIColor
    private var animationDuration: CFTimeInterval
    private var timeOffset: CFTimeInterval
    private var automaticStart: Bool
    
    required init(strokeColor: UIColor,
                  strokeBackgroundColor: UIColor = .clear,
                  cornerRadius: CGFloat = 0.0,
                  lineWidth: CGFloat = 4.0,
                  lineCap: CAShapeLayerLineCap = .round,
                  segmentLength: CGFloat = 0.4,
                  duration: CFTimeInterval,
                  timeOffset: CFTimeInterval = 0.0,
                  automaticStart: Bool = true) {
        self.strokeColor = strokeColor
        self.strokeBackgroundColor = strokeBackgroundColor
        self.cornerRadius = cornerRadius
        self.lineWidth = lineWidth
        self.lineCap = lineCap
        self.segmentLength = segmentLength
        self.animationDuration = duration
        self.timeOffset = timeOffset
        self.automaticStart = automaticStart
        super.init(frame: CGRect.zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if !readyForDrawing {
            firstTimeSetup()
        }
        if !isAnimating && automaticStart {
            startAnimating()
        }
    }
    
    private func firstTimeSetup() {
        shapeLayer = newShapeLayer(rectangle: bounds)
        shapeLayer.strokeColor = strokeBackgroundColor.cgColor
        shapeLayer.strokeStart = 0
        shapeLayer.strokeEnd = 1
        layer.addSublayer(shapeLayer)
        
        progressLayer1 = newShapeLayer(rectangle: bounds, lineCap: lineCap)
        progressLayer1.strokeColor = strokeColor.cgColor
        
        progressLayer2 = newShapeLayer(rectangle: bounds, lineCap: lineCap, rotation: 180)
        progressLayer2.strokeColor = strokeColor.cgColor
        
        readyForDrawing = true
    }
    
    private func newShapeLayer(rectangle: CGRect,
                               fillColor: UIColor = .clear,
                               lineCap: CAShapeLayerLineCap = .butt,
                               rotation: CGFloat = 0) -> CAShapeLayer {
        let layer = CAShapeLayer()
        let path = newPath(rectangle: rectangle, cornerRadius: cornerRadius, rotation: rotation)
        layer.path = path.cgPath
        layer.lineWidth = lineWidth
        layer.fillColor = fillColor.cgColor
        layer.lineCap = lineCap
        return layer
    }
    
    private func newPath(rectangle: CGRect, cornerRadius: CGFloat, rotation: CGFloat = 0) -> UIBezierPath {
        let path = UIBezierPath(roundedRect: rectangle, cornerRadius: cornerRadius)
        path.rotate(degree: rotation)
        return path
    }

    func startAnimating() {
        isAnimating = true
        
        layer.addSublayer(progressLayer1)
        
        progressLayer2.strokeStart = 0
        progressLayer2.strokeEnd = 0
        layer.addSublayer(progressLayer2)
        
        let strokeEndAnimation1 = CAKeyframeAnimation(keyPath: "strokeEnd")
        strokeEndAnimation1.values = [0, 1]
        strokeEndAnimation1.keyTimes = [0, 1]
        
        let strokeStartAnimation1 = CAKeyframeAnimation(keyPath: "strokeStart")
        strokeStartAnimation1.values = [0, 1]
        strokeStartAnimation1.keyTimes = [0, 1]
        strokeStartAnimation1.beginTime = animationDuration * segmentLength
        
        let animationGroup1 = CAAnimationGroup()
        animationGroup1.animations = [strokeEndAnimation1, strokeStartAnimation1]
        animationGroup1.isRemovedOnCompletion = false
        animationGroup1.duration = animationDuration
        animationGroup1.fillMode = .forwards
        animationGroup1.repeatCount = .infinity
        animationGroup1.timeOffset = timeOffset
        progressLayer1.add(animationGroup1, forKey: "animationGroup1")
        
        let strokeEndAnimation2 = CAKeyframeAnimation(keyPath: "strokeEnd")
        strokeEndAnimation2.values = [0, 1]
        strokeEndAnimation2.keyTimes = [0, 1]
        
        let strokeStartAnimation2 = CAKeyframeAnimation(keyPath: "strokeStart")
        strokeStartAnimation2.values = [0, 1]
        strokeStartAnimation2.keyTimes = [0, 1]
        strokeStartAnimation2.beginTime = animationDuration * segmentLength
        
        let animationGroup2 = CAAnimationGroup()
        animationGroup2.animations = [strokeEndAnimation2, strokeStartAnimation2]
        animationGroup2.isRemovedOnCompletion = false
        animationGroup2.duration = animationDuration
        animationGroup2.fillMode = .forwards
        animationGroup2.repeatCount = .infinity
        animationGroup2.beginTime = CACurrentMediaTime() + animationDuration / 2
        animationGroup2.timeOffset = timeOffset
        progressLayer2.add(animationGroup2, forKey: "animationGroup2")
    }

    func completeProgress() {
        progressLayer1.removeAllAnimations()
        progressLayer2.removeAllAnimations()
        progressLayer1.strokeStart = 0
        progressLayer1.strokeEnd = 1
    }
}

extension UIBezierPath {
    func rotate(degree: CGFloat) {
        let bounds: CGRect = self.cgPath.boundingBox
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        
        let radians = degree / 180.0 * .pi
        var transform: CGAffineTransform = .identity
        transform = transform.translatedBy(x: center.x, y: center.y)
        transform = transform.rotated(by: radians)
        transform = transform.translatedBy(x: -center.x, y: -center.y)
        self.apply(transform)
    }
}

You use it like this:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let segmentLenth = 0.4
        let duration: CFTimeInterval = 10
        let timeOffset: CFTimeInterval = segmentLenth * duration // or 0 if you don't mind it starting from the top left
        
        let indicator = RoundedRectActivityIndicator(strokeColor: .red,
                                                     strokeBackgroundColor: .orange.withAlphaComponent(0.2),
                                                     cornerRadius: 6,
                                                     lineWidth: 6,
                                                     segmentLength: segmentLenth,
                                                     duration: duration,
                                                     timeOffset: timeOffset)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(indicator)
        NSLayoutConstraint.activate([
            indicator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
            indicator.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
            indicator.topAnchor.constraint(equalTo: view.topAnchor, constant: 350),
            indicator.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -350)
        ])
        
//        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(8)) {
//            indicator.completeProgress()
//        }
    }
}

Result:

enter image description here

lawicko
  • 7,246
  • 3
  • 37
  • 49
0

This worked for me and it's working properly.

import UIKit

class ViewController: UIViewController {
 
    let cornerRadius: CGFloat = 12
    private var segmentLength: CGFloat = 0.4
    private var animationDuration: CFTimeInterval = 5
    private var timeOffset: CFTimeInterval = 10
    
    @IBOutlet weak var nView: UIView!
    override func viewDidLoad() {
        super.viewDidLoad()
        nView.layer.borderWidth = 1.0
        nView.layer.cornerRadius = cornerRadius
        nView.layer.borderColor = UIColor.black.cgColor
        call()
    }

    func call() {
        //Border Layer
        let borderShapeLayer = CAShapeLayer()
        borderShapeLayer.path = nPathBezier(rectangle: nView.bounds, cornerRadius: cornerRadius).cgPath
        borderShapeLayer.fillColor = UIColor.clear.cgColor
        borderShapeLayer.strokeColor = UIColor.red.cgColor
        borderShapeLayer.lineWidth = 5
        
        //Border Layer 1
        let borderShapeLayer1 = CAShapeLayer()
        borderShapeLayer1.path = nPathBezier(rectangle: nView.bounds, cornerRadius: cornerRadius, rotation: 180).cgPath
        borderShapeLayer1.fillColor = UIColor.clear.cgColor
        borderShapeLayer1.strokeColor = UIColor.red.cgColor
        borderShapeLayer1.lineWidth = 5
        borderShapeLayer1.strokeEnd = 0
        borderShapeLayer1.strokeStart = 0
        
        //End Animation
        let endAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
        endAnimation.values = [0, 1]
        endAnimation.keyTimes = [0, 1]
        
        //Start Animation
        let startAnimation = CAKeyframeAnimation(keyPath: "strokeStart")
        startAnimation.values = [0, 1]
        startAnimation.keyTimes = [0, 1]
        startAnimation.beginTime = animationDuration * segmentLength
        
        //Add Animation to borderShapeLayer1
        let anime1 = CAAnimationGroup()
        anime1.animations = [endAnimation, startAnimation]
        anime1.duration = animationDuration
        anime1.repeatCount = .infinity
        anime1.timeOffset = timeOffset
        borderShapeLayer.add(anime1, forKey: "anime1")
        
        //Add Animation to borderShapeLayer2
        let anime2 = CAAnimationGroup()
        anime2.animations = [endAnimation, startAnimation]
        anime2.duration = animationDuration
        anime2.repeatCount = .infinity
        anime2.beginTime = CACurrentMediaTime() + animationDuration / 2
        anime2.timeOffset = timeOffset
        borderShapeLayer1.add(anime2, forKey: "anime2")

        //Add layer into the view
        nView.layer.addSublayer(borderShapeLayer)
        nView.layer.addSublayer(borderShapeLayer1)
    }
    //Create UIBezierPath
    private func nPathBezier(rectangle: CGRect, cornerRadius: CGFloat, rotation: CGFloat = 0) -> UIBezierPath {
        let path = UIBezierPath(roundedRect: rectangle, cornerRadius: cornerRadius)
        path.rotate(degree: rotation)
        return path
    }
}
extension UIBezierPath {
    func rotate(degree: CGFloat) {
        let bounds: CGRect = self.cgPath.boundingBox
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radians = degree / 180.0 * .pi
        var transform: CGAffineTransform = .identity
        transform = transform.translatedBy(x: center.x, y: center.y)
        transform = transform.rotated(by: radians)
        transform = transform.translatedBy(x: -center.x, y: -center.y)
        self.apply(transform)
    }   
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278