티스토리 뷰

https://iostutorialjunction.com/2018/05/how-to-create-circular-progress-view-swift-tutorial.html

 

Create circular progress view in swift – Tutorial – iOSTutorialJunction

Circular progress view In this tutorial, we will learn how to create our own circular progress view or bar in swift. Though, a number of open source libraries are available for the circular progress view written in swift, but this tutorial demonstrate that

iostutorialjunction.com

일단 이번 포스트는 위 포스트를 참고해서 만들었습니다.

다만 일부를 변경해서 사용하기 때문에 둘중에 더 편하신걸 사용하시면 될 것 같아요.  

 

그래서 오늘은 무얼 만드냐면 아래처럼 생긴걸 만들까 합니다. (퍼센트에 따라 노란 부분이 많아지는 구조로 만들겁니다.)

이 도너츠 모양을 만들겁니다.

모양은 비슷하게? 만들꺼에요!

(다만 제가 좀더 편하게 사용하기 위해 일부를 변경할 껍니다.)

 

저는 좀더 둥글둥글하고 사용하기 편하게 일부 코드를 변경했습니다. 

(중간에 꼬이는 바람에 생각보다 시간이 좀 걸렸네요.)

사용방법은 아래에 추가로 작성하겠습니다. 

 

일단 제가 만든 코드를 적용하는 방법부터 천천히 설명 드릴께요.

(모서리가 둥근게 싫으신 분들은 코드의 설명을 참고해 고쳐주시면 됩니다.)

 

각 코드의 주석을 참고하시면 쉽게 원하는 스타일로 변경하실 수 있어요!

CircularProgress 전체코드

import Foundation
import UIKit


extension UIBezierPath { // 타원의 경우 대비

    convenience init(ovalInRect rect: CGRect, startAngle: CGFloat, clockwise: Bool) {
        self.init()
        // Create a circle at the origin with diameter 1.
        addArc(withCenter: .zero, radius: 0.5, startAngle: startAngle, endAngle: startAngle + 2 * CGFloat(Double.pi), clockwise: clockwise)
        close()

        // Construct a transform that moves the circle to inscribe `rect`.
        var transform = CGAffineTransformIdentity
        // This part moves the center of the circle to the center of `rect`.
        transform = CGAffineTransformTranslate(transform, rect.midX, rect.midY)
        // This part scales the circle to an oval with the same width and height as `rect`.
        transform = CGAffineTransformScale(transform, rect.width, rect.height)

        apply(transform)
    }

}
class CircularProgress: UIView, CAAnimationDelegate {

    fileprivate var progressLayer = CAShapeLayer()
    fileprivate var tracklayer = CAShapeLayer()
    
    // 트랙 색깔
    @IBInspectable var trackColor: UIColor {
        set {
            tracklayer.strokeColor = newValue.cgColor
        }
        get {
            return UIColor(cgColor: tracklayer.strokeColor ?? UIColor.clear.cgColor)
        }
    }
    // 트랙 두께
    @IBInspectable var trackLineWidth: CGFloat {
        set {
            tracklayer.lineWidth = newValue
        }
        get {
            return tracklayer.lineWidth
        }
    }
    // 진행률 표시 색깔
    @IBInspectable var progressLineColor: UIColor {
        set {
            progressLayer.strokeColor = newValue.cgColor
        }
        get {
            return UIColor(cgColor: progressLayer.strokeColor ?? UIColor.clear.cgColor)
        }
    }
    // 진행률 표시 두께
    @IBInspectable var progressLineWidth: CGFloat {
        set {
            progressLayer.lineWidth = newValue
        }
        get {
            return progressLayer.lineWidth
        }
    }
    // 진행률 값
    var moveValueTo = 0.5 // 기본값
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    override func draw(_ rect: CGRect) {
        // 뷰 사이즈가 정확하게 나오게 하기 위해서
        self.createCircularPath()
    }

    fileprivate func createCircularPath() {
        self.backgroundColor = UIColor.clear

        let circlePath = UIBezierPath(ovalInRect: CGRect(x: 0, y: 0, width: (frame.size.width) , height: (frame.size.height) - tracklayer.cornerRadius), startAngle: CGFloat(-0.5 * Double.pi), clockwise: true)// 시계방향으로 채우기
        // 트랙 설정
        tracklayer.path = circlePath.cgPath
        tracklayer.fillColor = UIColor.clear.cgColor

        tracklayer.strokeEnd = 1.0
        layer.addSublayer(tracklayer)
        // 진행률 설정
        progressLayer.path = circlePath.cgPath
        //MARK: 여기를 round/square/butt 로 변경시 모서리가 모양이 달라집니다.
        progressLayer.lineCap = .round
        progressLayer.fillColor = UIColor.clear.cgColor

        progressLayer.strokeEnd = self.moveValueTo//0.4
        
        layer.addSublayer(progressLayer)
    }
    
    func setProgress(value: Double) {
        self.moveValueTo = value
        progressLayer.strokeEnd = CGFloat(value)
        self.createCircularPath()
        
    }
    
    //0부터 목표까지 애니메이션작동
    func setProgressWithAnimation(duration: TimeInterval, value: Float) {
        self.moveValueTo = Double(value)
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = duration
        // Animate from 0 (no circle) to 1 (full circle)
        animation.fromValue = 0
        animation.toValue = value
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
        progressLayer.strokeEnd = CGFloat(value)
        progressLayer.add(animation, forKey: "animateCircle")
    }
    // 지정부분부터 목표까지 애니메이션 작동
    func setProgressWithAnimation(duration: TimeInterval,fromValue: Float ,toVlaue : Float){
        self.moveValueTo = Double(toVlaue)
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = duration
        // Animate from 0 (no circle) to 1 (full circle)
        animation.fromValue = fromValue//0
        animation.toValue = toVlaue
        animation.delegate = self
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
        progressLayer.strokeEnd = CGFloat(toVlaue)
        progressLayer.add(animation, forKey: "animateCircle")
    }
    
}

사용법 

Main (stroryboard) 설정 및 ViewController 코드 설정 

1. 원하시는 위치에 UIView 를 추가해주세요. 

원하시는 공간에 UIView를 추가해주세요.

2. 해당 UIView의 Class을 CircularProgress 로 변경해주세요. 

여기 보이시는 곳을 위처럼 수정해주시면 됩니다.

3. 원하시는 조건을 설정해주세요. (저는 보여드리기위해 색을찐하게 넣어볼께요.)

  • Track Color : 원이 생성될 길의 색깔
  • Track Line Width : 원이 생성될 길의 두께
  • Progress Line Color : 그려질 선의 색깔
  • Progress Line Width : 그려질 선의 두께  
  • View - BackGround 는 투명하게 나오도록 Default 를 넣어주세요.
  • 위 항목들은 코드로도 지정할수있습니다.

여기까지 하시면 아래처럼 보이시게 됩니다. (기본설정이 50% 로 되어있어서 반만 색칠된거에요!)

잘 보이시나요?

* 참고 원의 위치와 두께 

위와 같은 구조라 생각하시면 됩니다.

- 위 그림에서 네모가 여러분이 설정하신 뷰이고 색깔이 들어간 부분이 저희의 원이 그려지는 곳입니다. 

- 따라서 설정에 따라 원이 의도치않게 커지거나 짤릴수있으니 주의해서 사용해주세요. 

 

4. ViewController 에 코드를 추가해주세요.

- 저희가 원하는 %를 입력할 수 있어야 자유롭게 사용할 수 있겠죠?

- 그리고 애니메이션 효과를 원하시면 코드가 필요해요!

- 물론 그전에 스토리보드에 해당 뷰를 연결해야겠죠?

// 저는 일단 이름을 이렇게 지을께요
@IBOutlet weak var circleView: CircularProgress!

- 코드 설명은 주석을 참고해주세요.

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    //MARK: 스토리보드에서 했던 설정 변경
    //스토리보드 - track color
    self.circleView.trackColor = .purple
    //스토리보드 - track Line Width
    self.circleView.trackLineWidth = 20.0
    //스토리보드 - progress Line Color
    self.circleView.progressLineColor = .yellow
    //스토리보드 - progress Line Width
    self.circleView.progressLineWidth = 20.0
    // 배경색을 스토리보드에서 지정 안하셨으면 여기서 해주세요.
    self.circleView.backgroundColor = .clear
    //MARK: 애니메이션 효과를 사용하실분들은 0 /그냥 표기하실분들은 수치를 입력해주세요(0.0 ~. 1.0)
    // 몇퍼센트를 표시할지 지정합니다. (0.0 ~ 1.0)
    self.circleView.setProgress(value: 0.0)
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // MARK: 셋중에 원하시는 걸로 골라서 사용하시면 됩니다.
    // 70% 진행한 상태로 그려주기 (애니메이션 X) - 위에서 설정했으면 안해도 상관 X
    self.circleView.setProgress(value: 0.7) // 필수 아닙니다.
    // 10%~70% 까지 10초간 애니메이션 진행
    self.circleView.setProgressWithAnimation(duration: 10, fromValue: 0.1, toVlaue: 0.7)
    // 0%~70% 까지 10초간 애니메이션 진행
    self.circleView.setProgressWithAnimation(duration: 10, value: 0.7)
}

- setProgress(value:) 는 말그대로 해당 수치를 표시하는 함수입니다. 애니메이션 효과 없이 바로 그려줍니다.

- 여기까지 하시면 아래처럼 보이게 됩니다. (애니메이션 효과를 넣는 위치에 주의해주세요.)

색상과 애니메이션이 추가된게 보이나요?

- 여기까지가 기본 사용법입니다. 

*활용*

만약 그래프의 수치가 상승하면서 이를 UILabel 로 보여주고 싶으면 어떻게 해야할까요?

물론 여러가지 방법이 있겠지만 가장 간단한 방법은 바뀐 수치를 이용해

setProgress 를 계속 바꾸게 하는 방법이 있습니다. 

 

UITimer 를 이용해 계속 갱신하는 방법으로 애니메이션을 만들어 볼께요. 

 

1. 작동시킬 타이머와 UILabel 을 만들어 주세요. 

var animationTimer : Timer? //작동할 타이머
@IBOutlet weak var percentLbl: UILabel! // 퍼센트 표시용 라벨

2. 아래처럼 함수를 하나 만들어주세요.

func setAnimationWithTimer(){
    var i = 0.0
    self.circleView.setProgress(value: 0.0)
    // 0.01 초마다 업데이트 0.01초마다 0.1 퍼센트씩 증가 
    animationTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true){_ in
        i += 0.001
        if(i<1){//
            self.percentLbl.text = String(Int(i*100))+"%"
            self.circleView.setProgress(value: i)
        }else{
            self.animationTimer?.invalidate()
        }
    }
}

3. 해당 함수를 편하신 곳에 넣어주세요. (저는 viewDidLoad 마지막에 넣었습니다.)

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
   .
   .
   .
    self.setAnimationWithTimer()
}

4. 여기까지만 해주시면 아래처럼 보이게 됩니다.

글씨와 같이 바뀌시는게 보이시나요?

물론 기본적인 애니메이션이 필요하신 분들은

setProgressWithAnimation 으로도 충분하실 수 있지만

기본적인 setProgress를 활용해 저런 식으로 응용할수있습니다. 

(예를 들어 사용자에게 다운로드 상황을 보여줄때 사용하면 편하겠죠?)

 

 다른 분들도 쉽게 사용하셨으면 하는 마음에서 열심히 바꿔 봤는데

도움이 되기를 바랍니다. 

(이거 바꾸다 헤메서 오류 천국에 이틀간 갇혀있었어요.....)

 

아무튼 이 글이 도움이 되시길 바라며 

오늘도 파이팅입니다. 

댓글