본문 바로가기
iOS/예제

[iOS 예제] CircleProgressView 만들기

by Sky Titan 2021. 12. 11.
728x90

만들고자 하는 View는 위와 같은 형태로 현재까지 어떤 특정한 작업의 진행 정도를 시각적으로 보여주는 뷰이다.

 

 

CirlcleLayer

class CircleLayer: CALayer {
    
    public var progressWidth: CGFloat = 0
    public var progressColor: UIColor?
    public var progressBackgroundColor: UIColor?
    public var progress: CGFloat = 0
    public var clockwise: Bool = false
    public var progressLayer: CAShapeLayer?
    var center: CGPoint {
        return CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
    }
    var radius: CGFloat {
        return min(frame.size.width, frame.size.height) / 2
    }
    private let startAngle: Radians = Degrees(0).toRadians()
    private let endAngle: Radians = Degrees(360).toRadians()
    
    override func draw(in ctx: CGContext) {
        super.draw(in: ctx)
        
        addCenterCircle(ctx)
        addProgressBackground(ctx)
        
        if progressLayer == nil {
            let progressLayer = CAShapeLayer()
            self.progressLayer = progressLayer
        
            setProgressLayerStyle()
            addSublayer(progressLayer)
        } else {
            setProgressLayerStyle()
        }
    }
    
    private func addCenterCircle(_ ctx: CGContext) {
        ctx.addArc(center: center, radius: radius - progressWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
        
        ctx.setFillColor(UIColor.white.cgColor)
        ctx.fillPath()
    }
    
    private func addProgressBackground(_ ctx: CGContext) {
        ctx.addArc(center: center, radius: radius - progressWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
        ctx.setLineWidth(progressWidth)
        ctx.setStrokeColor(progressBackgroundColor?.cgColor ?? UIColor.white.cgColor)
        ctx.strokePath()
        ctx.fillPath()
    }
    
    private func setProgressLayerStyle() {
        progressLayer?.path = UIBezierPath(arcCenter: center, radius: radius - progressWidth / 2, startAngle: startAngle - Degrees(90).toRadians(), endAngle: endAngle - Degrees(90).toRadians(), clockwise: clockwise).cgPath
        progressLayer?.lineCap = .round
        progressLayer?.lineWidth = progressWidth
        progressLayer?.fillColor = UIColor.clear.cgColor
        progressLayer?.strokeColor = progressColor?.cgColor
        progressLayer?.strokeStart = 0
        progressLayer?.strokeEnd = progress
        progressLayer?.setNeedsDisplay()
    }
    
    func animate() {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = progress
        animation.repeatCount = 1
        animation.duration = 0.7
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        progressLayer?.add(animation, forKey: "StrokeAnimation")
    }
}

CircleProgressView의 layer가 될 CircleLayer이다.

 

크게 세가지 부분으로 구분된다.

1. 중앙 부분의 하얀색 원 (CenterCircle)

2. Progress Cirlce

3. Progress Cirlce의 뒷배경 (ProgressBackground)

 

여기서 Progress Circle은 따로 애니메이션을 넣을 것이므로 ctx로 그리지 않고 CAShapeLayer로 만들어서 sublayer로 추가했다.

 

addArc로 원을 그리되 fillColor는 쓰지 않고 storkeColor와 strokeStart, strokeEnd를 써서 현재 progress만큼 원의 테두리가 그려지게 했다.

 

CircleProgressView

public class CircleProgressView: UIView {
    
    @IBInspectable
    public var progressColor: UIColor? {
        didSet {
            circleLayer.progressColor = progressColor
            circleLayer.setNeedsDisplay()
        }
    }
    @IBInspectable
    public var progressBackgroundColor: UIColor? {
        didSet {
            circleLayer.progressBackgroundColor = progressBackgroundColor
            circleLayer.setNeedsDisplay()
        }
    }
    @IBInspectable
    public var progress: CGFloat = 0 {
        didSet {
            circleLayer.progress = progress
            circleLayer.setNeedsDisplay()
        }
    }
    @IBInspectable
    public var progressWidth: CGFloat = 0 {
        didSet {
            circleLayer.progressWidth = progressWidth
            circleLayer.setNeedsDisplay()
        }
    }
    @IBInspectable
    public var clockwise: Bool {
        set {
            circleLayer.clockwise = !newValue
            circleLayer.setNeedsDisplay()
        }
        
        get {
            return !circleLayer.clockwise
        }
    }
    
    private var circleLayer: CircleLayer {
        return layer as! CircleLayer
    }
    
    public override class var layerClass: AnyClass {
        return CircleLayer.self
    }
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    public func startProgressAnimate() {
        circleLayer.animate()
    }
}

CircleLayer가 들어가는 CircleProgressView는

progress

progressWidth

progressColor

progressBackgroundColor

clockwise

위의 값들을 IBInspectable property로 선언하여 Xib에서 값을 설정할 수 있도록 했다.

 

그리고 clockwise 값은 이상하게도 원을 그릴 때 분명 시계방향으로 그려져야 하는데 시계반대방향으로 그려지고

시계반대방향으로 설정하면 시계방향으로 그려져서 set 블록에서 반대로 set하도록 했다.

 

 

아래는 strokeEnd 애니메이션 효과이다

 

 

728x90

댓글