적당한 고통은 희열이다

- 댄 브라운 '다빈치 코드' 중에서

초보 iOS 개발자의 일상/개발 업무

[Swift iOS] circular progress bar 원형 프로그래스 바 만들기 (feat. UIBezierPath)

hongssup_ 2023. 11. 3. 16:09
반응형

2023.11.03 (금)

미션을 수행하면 표시될 progress bar 작업을 진행했다.

 

 

이미지 위에 원형 프로그래스 바를 직접 구현하여 미션 수행 상황을 한 눈에 볼 수 있도록 하였다.

미션 수행 내역이 한 번이라도 있는 경우, 이미지 위에 회색 및 흰색 테두리가 표시되고, 그 위에 파란색으로 진행상황을 표시해주는 방식이다.

 

1. CircularProgressBar 클래스 생성

다음과 같이 클래스로 원형 프로그래스 바를 생성해두면, var progressBar = CircularProgressBar() 이런 식으로 간편하게 사용이 가능하다. 

class CircularProgressBar: UIView {
    private let lineWidth: CGFloat = 2
}

 

2. 프로그래스가 표시될 기본 배경 stroke 그려주기

draw(_:) 에서 UIBezierPath 를 이용해 배경 stroke 를 그려준다.

이 draw 메서드는 UIView에서 기본 구현이 되어 있어, 따로 호출하지 않아도 CircularProgressBar 가 초기화 될 때 자동으로 불리기 때문에 배경 stroke 만 우선 먼저 그려주자. 

draw 메서드 내에서는 해당 view 의 frame이 자동으로 반영이 되어 따로 rect 를 설정해 줄 필요가 없다. 

override func draw(_ rect: CGRect) {
    super.draw(rect)
    
    let path = UIBezierPath()
    path.addArc(withCenter: CGPoint(x: rect.midX, y: rect.midY), 
                radius: rect.midX - (lineWidth / 2),
                startAngle: 0,
                endAngle: 2 * .pi,
                clockwise: true)
    path.lineWidth = lineWidth
    MacaColors.gray200.set()
    path.stroke()
}

UIBezierPath 에 addArc 를 사용해 path 를 그려준다.

- withCenter : 그려줄 곡선의 중심점

- radius : 곡선의 반지름

- startAngle : 곡선의 시작점 (* 0으로 설정하면 12시 방향이 아니고 9시 방향임. 아래에서 설명 계속,,) 

- endAngle : 곡선의 종료점

- clockwise : 시계 방향 / 반시계 방향

 

3. 프로그래스 바 그려주기

func setProgress(diameter: CGFloat, progress: Double) {
    backgroundColor = .clear
    
    let rect = CGRect(x: .zero, y: .zero, width: diameter, height: diameter)
    let startAngle = CGFloat.pi / 2
    
    let circularPath = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY),
                                    radius: rect.midX - (lineWidth / 2),
                                    startAngle: -startAngle,
                                    endAngle: (.pi * 2) * progress - startAngle,
                                    clockwise: true)
    let circularLayer = CAShapeLayer()
    circularLayer.path = circularPath.cgPath
    circularLayer.fillColor = UIColor.clear.cgColor
    circularLayer.strokeColor = MacaColors.skyBlue700.cgColor
    circularLayer.lineWidth = lineWidth
    //circularLayer.lineCap = .round
    
    self.layer.addSublayer(circularLayer)
}

1) 곡선을 그릴 위치와 크기 설정 - CGRect

CGRect 를 self.bounds 나 frame 을 사용해서 설정해주니까 width, height 값이 0으로 적용되어 곡선이 제대로 그려지지 않았다. 

그래서 그려줄 곡선의 지름을 받아와 rect 를 설정해 주었다. 

 

2) 시작점 설정

startAngle 을 0으로 설정해두면 12시 방향이 아니라 9시 방향에서부터 시작하기 때문에, 12시 방향에서부터 시작하고 싶다면

시작점을 -CGFloat.pi / 2 로 설정해두어야 함. 

 

3) 종료 위치 설정

** 파이 너무 오랜만이라 헷갈려서 다시 찾아봄,, ㅎㅎ 파이(π)는 원주율, 즉 원의 지름에 대한 둘레의 비율을 뜻한다.

endAngle 을 설정할 때, 원의 둘레에 progress 를 곱한 만큼 표시를 해주어야 하므로 (.pi * 2) * progress 이렇게 설정을 해줘야 한다. 그리고 startAngle이 0 이 아니었으니깐 여기서 startAngle 만큼 또 빼주기. 

 

4) 레이어에 path 입히기 - CAShapeLayer()

위의 draw 메서드에서는 자동으로 그려주었지만, path로 입혀줘야댐 (아닌가? 다시 확인해보자)

그려준 곡선 path 를 레이어에 입힌 후, addSublayer로 레이어를 추가해준다. 

 

 

+ 나는 그냥 view 가 세팅될 때 프로그래스 상황을 초기화 해주기만 하면 되니까 setProgress 함수를 만들어 구현을 해주었지만, 

progress value 에 따라 프로그래스 바가 계속 업데이트 되어야 한다면 value 값이 변경될 때마다 프로그래스 바를 갱신할 수 있도록 didset 이나 옵저버블을 사용하여 동기화를 시켜줘도 좋을 듯 하다. 

 

 


처음에 시안 보고 매우 귀찮아 보였는데, 하고 보니 생각보다 별로 안어렵고 재밌었둠 ㅎㅎ 

예뿌다 

 

 

 

코드 전문

final class CircularProgressBar: UIView {
    private let lineWidth: CGFloat = 2

    func setProgress(diameter: CGFloat, progress: Double) {
        backgroundColor = .clear
        
        let rect = CGRect(x: .zero, y: .zero, width: diameter, height: diameter)
        let startAngle = CGFloat.pi / 2
        
        let circularPath = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY),
                                        radius: rect.midX - (lineWidth / 2),
                                        startAngle: -startAngle,
                                        endAngle: (.pi * 2) * progress - startAngle,
                                        clockwise: true)
        let circularLayer = CAShapeLayer()
        circularLayer.path = circularPath.cgPath
        circularLayer.fillColor = UIColor.clear.cgColor
        circularLayer.strokeColor = MacaColors.skyBlue700.cgColor
        circularLayer.lineWidth = 2
        //circularLayer.lineCap = .round
        
        self.layer.addSublayer(circularLayer)
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let outerPath = UIBezierPath()
        outerPath.addArc(withCenter: CGPoint(x: rect.midX, y: rect.midY), 
                         radius: rect.midX - (lineWidth / 2),
                         startAngle: 0,
                         endAngle: 2 * .pi,
                         clockwise: true)
        outerPath.lineWidth = lineWidth
        MacaColors.gray200.set()
        outerPath.stroke()
        
        let innerPath = UIBezierPath()
        innerPath.addArc(withCenter: CGPoint(x: rect.midX, y: rect.midY), 
                         radius: rect.midX - lineWidth - (lineWidth / 2),
                         startAngle: 0,
                         endAngle: 2 * .pi,
                         clockwise: true)
        innerPath.lineWidth = lineWidth
        UIColor.white.set()
        innerPath.stroke()
    }
}
728x90
반응형