-2

I'm trying to create a Custom Progress Bar View and the idea is to have a timer that will update every second. However, the transition from the progress is not so smooth.

If I reduce the timer interval to something like 0.01 instead of 0.1 the "animation" works nicely; however, I'm trying to avoid it.

There's some way to make this animation smooth using 1 second on the Timer interval?

enter image description here

class CustomProgressBar: UIView {
    
    private let progressLayer = CALayer()
    
    var progress: CGFloat = 1.0 {
        didSet {
            setNeedsDisplay()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSublayers()
        startTimer()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addSublayers()
        startTimer()
    }
    
    private func startTimer() {
        Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateProgressBar), userInfo: nil, repeats: true)
    }
    
    override func draw(_ rect: CGRect) {
        let backgroundMask = CAShapeLayer()
        backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.5).cgPath
        layer.mask = backgroundMask
        
        updateLayer(rect: rect, color: .white)
        
        backgroundColor = .black
        progressLayer.backgroundColor = UIColor.white.cgColor
    }
    
    private func addSublayers() {
        layer.addSublayer(progressLayer)
    }
    
    @objc func updateProgressBar() {
        progress -= 0.1
    }
    
    private func updateLayer(rect: CGRect, color: UIColor?) {
        
        let progressRectBorderSize = rect.height * 0.2
        let width: CGFloat
        width = max(rect.width * progress - (progressRectBorderSize * 2), 0)
        
        let progressRect = CGRect(origin: CGPoint(x: progressRectBorderSize, y: progressRectBorderSize), size: CGSize(width: width, height: rect.height - (progressRectBorderSize * 2)))
        
        let progressBackgroundMask = CAShapeLayer()
        let progressBackgroundMaskRoundedRect = CGRect(x: 0, y: 0, width: progressRect.width, height: progressRect.height)
        progressBackgroundMask.path = UIBezierPath(roundedRect: progressBackgroundMaskRoundedRect, cornerRadius: progressRect.height * 0.5).cgPath
        progressLayer.mask = progressBackgroundMask
        
        progressLayer.frame = progressRect
    }
}
chr0x
  • 1,151
  • 3
  • 15
  • 25

1 Answers1

1

Here is how I create CustomProgressBar for you according to your needs.

I gave it a run, it works smoothly. You can customize it according to your Design needs.


class CustomProgressBar: UIView {
    
    private var progressLayerBackground = CAShapeLayer()
    private var progressLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func draw(_ rect: CGRect) {
        
        progressLayerBackground = createBarShapeLayer(strokeColor: .black, rect: rect, margin: 10)
        layer.addSublayer(progressLayerBackground)
        
        progressLayer = createBarShapeLayer(strokeColor: .white, rect: rect, margin: 10)
        progressLayer.lineWidth = 15
        progressLayer.strokeEnd = 0 // This define how much will be width of bar at start.
        layer.addSublayer(progressLayer)
        animateBar()
                
        backgroundColor = .white
        progressLayerBackground.backgroundColor = UIColor.white.cgColor
    }
    
    private func addSublayers() {
        layer.addSublayer(progressLayerBackground)
    }
    
    private func createBarShapeLayer(strokeColor: UIColor, rect: CGRect, margin: CGFloat) -> CAShapeLayer {
        let layer = CAShapeLayer()
        
        let linePath = UIBezierPath()
        linePath.move(to: CGPoint(x: rect.width - margin, y: rect.height /  2))
        linePath.addLine(to: CGPoint(x: rect.minX + margin , y: rect.height /  2))
        
        layer.path = linePath.cgPath
        layer.strokeColor = strokeColor.cgColor
        layer.lineWidth = 20
        layer.strokeEnd = 1
        layer.lineCap = .round
        
        return layer
    }
    
    private func animateBar() {
        let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        
        basicAnimation.toValue = 1
        basicAnimation.duration = 2 // This defines how much will be duration of animation.
        basicAnimation.fillMode = .forwards
        basicAnimation.beginTime = CACurrentMediaTime() + 0.5
        basicAnimation.isRemovedOnCompletion = false
        progressLayer.add(basicAnimation, forKey: "moveHorizontally")
    }
}

Demo: https://drive.google.com/file/d/1iGwcPPe7uQROa9s276t4SIsHY42fTJ1D/view?usp=sharing

Aafaq
  • 202
  • 1
  • 13