iOS에서 원형 진행률 표시줄 만들기
iOS 개발자라면 원형 진행률 표시줄(circular progress bar)을 만드는 방법은 반드시 익혀야 할 필수 스킬입니다. 실제로 거의 모든 앱에서 이 요소를 사용하고 있죠.
원형 진행률 표시줄은 주로 파일 다운로드 상태, 로딩 상태 등 진행 상황을 시각적으로 보여줄 때 활용됩니다.
하지만 처음 접하는 개발자에게는 구현 과정이 까다롭게 느껴질 수 있습니다. 다행히 원형 진행률 표시줄을 만드는 방법은 여러 가지가 있으며, 이 글에서는 그중 가장 간단하고 쉬운 방법을 소개합니다.
그럼 바로 시작해 보겠습니다!
1단계 – Xcode 프로젝트 생성
Xcode를 열고 Single View Application 템플릿으로 새 프로젝트를 생성한 뒤, 프로젝트 이름을 CircularProgress로 지정합니다.
이번 튜토리얼에서는 30%, 60%, 95% 세 개의 버튼과 하나의 원형 진행률 뷰를 가진 앱을 만듭니다. 버튼을 탭하면 해당 백분율만큼 진행률 뷰가 변경됩니다.
2단계 – CircularProgressView 클래스 추가
메뉴에서 File → New → File → Cocoa Touch Class를 선택하고, UIView를 상속받는 CircularProgressView 클래스를 새로 만듭니다.
3단계 – UI 구성
스토리보드에 UIView를 하나 추가하고, 위에서 만든 CircularProgressView 클래스를 연결합니다. 이어서 버튼 세 개를 추가하고 각각 30%, 60%, 95%로 이름을 지정합니다.
ViewController.swift에서 세 버튼에 대한 @IBAction을 아래와 같이 생성합니다.
@IBAction func btn95(_ sender: Any) {
}
@IBAction func btn30(_ sender: Any) {
}
@IBAction func btn60(_ sender: Any) {
}
그리고 UIView에 대한 @IBOutlet도 아래처럼 선언합니다.
@IBOutlet weak var circularProgress: CircularProgressView!
4단계 – CAShapeLayer 객체 생성
CircularProgressView.swift 파일 안에서 CAShapeLayer 타입의 두 객체, 즉 progressLyr(진행 레이어)와 trackLyr(트랙 레이어)를 선언합니다.
var progressLyr = CAShapeLayer() var trackLyr = CAShapeLayer()
5단계 – didSet으로 색상 속성 처리
progressClr과 trackClr 속성에 didSet 옵저버를 작성해 레이어 색상이 자동으로 갱신되도록 합니다.
var progressClr = UIColor.white {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.white {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
여기서는 progressLyr과 trackLyr의 색상 속성을 설정하고 있습니다.
didSet은 프로퍼티 옵저버(property observer)입니다. 프로퍼티 옵저버는 프로퍼티 값의 변화를 감지하고 대응하며, 새 값이 기존 값과 같더라도 값이 설정될 때마다 호출됩니다.
6단계 – makeCircularPath 함수 작성
아래 코드를 추가해 원형 경로를 만드는 함수를 구현합니다.
func makeCircularPath() {
self.backgroundColor = UIColor.clear
self.layer.cornerRadius = self.frame.size.width/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width/2, y: frame.size.height/2), radius: (frame.size.width - 1.5)/2, startAngle: CGFloat(-0.5 * .pi), endAngle: CGFloat(1.5 * .pi), clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
progressLyr.path = circlePath.cgPath
progressLyr.fillColor = UIColor.clear.cgColor
progressLyr.strokeColor = progressClr.cgColor
progressLyr.lineWidth = 10.0
progressLyr.strokeEnd = 0.0
layer.addSublayer(progressLyr)
}
이 함수에서는 원형 경로를 생성하고, 경로의 각종 파라미터와 동작 방식을 정의합니다.
7단계 – required init 추가
스토리보드로 UI를 설계할 때는 required init을 사용하고, 코드로 UI를 직접 그릴 때는 override init을 사용합니다. 우리는 스토리보드를 사용하므로 required init을 구현합니다.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
makeCircularPath()
}
8단계 – 진행률 애니메이션 함수 작성
이제 진행률에 애니메이션을 적용하기 위해 setProgressWithAnimation 함수를 새로 만들고 아래 코드를 작성합니다.
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
progressLyr.strokeEnd = CGFloat(value)
progressLyr.add(animation, forKey: "animateprogress")
}
CircularProgressView.swift 최종 코드
여기까지 완료했습니다! CircularProgressView.swift의 전체 코드는 아래와 같아야 합니다.
import UIKit
class CircularProgressView: UIView {
var progressLyr = CAShapeLayer()
var trackLyr = CAShapeLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
makeCircularPath()
}
var progressClr = UIColor.white {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.white {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
func makeCircularPath() {
self.backgroundColor = UIColor.clear
self.layer.cornerRadius = self.frame.size.width/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width/2, y: frame.size.height/2), radius: (frame.size.width - 1.5)/2, startAngle: CGFloat(-0.5 * .pi), endAngle: CGFloat(1.5 * .pi), clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
progressLyr.path = circlePath.cgPath
progressLyr.fillColor = UIColor.clear.cgColor
progressLyr.strokeColor = progressClr.cgColor
progressLyr.lineWidth = 10.0
progressLyr.strokeEnd = 0.0
layer.addSublayer(progressLyr)
}
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
progressLyr.strokeEnd = CGFloat(value)
progressLyr.add(animation, forKey: "animateprogress")
}
}
9단계 – 빌드 후 화면 확인
코드를 실행해 정상적으로 동작하는지 확인합니다. 아직 ViewController.swift에 코드를 넣지 않았으므로 아래와 같은 UI가 보이지만 버튼은 아직 동작하지 않습니다.
10단계 – ViewController.swift에 코드 추가
이제 ViewController.swift에 코드를 추가해 보겠습니다.
viewDidLoad()에 아래 두 줄을 작성해 진행률 표시줄의 색상을 지정합니다.
circularProgress.trackClr = UIColor.cyan circularProgress.progressClr = UIColor.purple
그리고 각 버튼의 액션 메서드에 duration과 함께 95%, 30%, 60% 값을 넣어 아래처럼 작성합니다.
@IBAction func btn95(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.95)
}
@IBAction func btn30(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.30)
}
@IBAction func btn60(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.60)
}
ViewController.swift 최종 코드
완성된 ViewController.swift의 전체 코드는 아래와 같습니다.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var circularProgress: CircularProgressView!
override func viewDidLoad() {
super.viewDidLoad()
circularProgress.trackClr = UIColor.cyan
circularProgress.progressClr = UIColor.purple
}
@IBAction func btn95(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.95)
}
@IBAction func btn30(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.30)
}
@IBAction func btn60(_ sender: Any) {
circularProgress.setProgressWithAnimation(duration: 1.0, value: 0.60)
}
}
각 버튼 함수에서는 값(value)과 지속 시간(duration)을 인자로 setProgressWithAnimation을 호출합니다.
모두 완료되었습니다! 앱을 실행한 뒤 30%, 60%, 95% 버튼 중 하나를 탭해 보세요. 원형 진행률 뷰가 부드럽게 애니메이션되는 것을 확인할 수 있습니다.