skip to Main Content

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)

2

Answers


  1. 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

    Login or Signup to reply.
  2. This worked for me and it’s working properly. Try this!

    [![> 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)
    }
    

    }

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search