[ios - Swift] Phi Chart View (CALayer)
()
이번 글에서는 지난번에 배운 UIBezierPath, CAShapeLayer를 사용해 파이 차트를 만들어 보도록 하겠습니다.
import UIKit
class UIPieChatView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func setNeedsDisplay() {
super.setNeedsDisplay()
definePieChatLayer()
}
private func definePieChatLayer() {
}
}
UIView를 상속받은 기본적인 클래스 구성입니다.
앞으로 이 클래스에 사용자가 원하는 값을 넣게 되면 우리는 setNeedsDisplayer()를 호출해 뷰를 다시 그려줄 수 있도록 하면 됩니다.
class UIPieChatView: UIView {
private let pieBezierPath = UIBezierPath()
var colors: [CGColor]? {
didSet {
setNeedsDisplay()
}
}
var values: [CGFloat]? {
didSet {
setNeedsDisplay()
}
}
중략 ....
파이 차트의 원하는 컬러와 밸류 값을 배열 형태로 추가해 줄 수 있도록 합니다.
파이 차트를 채워줄 베지어 패스도 마찬가지로 추가하고 이제 setNeedsDisplayer()를 호출 시 definePieChatLayer()를 호출해서 레이어를 추가해주면 됩니다.
private func definePieChatLayer() {
guard let values = values else { return }
guard let colors = colors else { return }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
var sAngle: CGFloat = 0
var eAngle: CGFloat = 0
for i in 0..<values.count {
sAngle = eAngle
eAngle = sAngle + values[i]
pieBezierPath.removeAllPoints()
pieBezierPath.move(to: center)
pieBezierPath.addArc(withCenter: center, radius: 150, startAngle: sAngle * 2 * CGFloat.pi, endAngle: eAngle * 2 * CGFloat.pi, clockwise: true)
pieBezierPath.close()
let pieLayer = CAShapeLayer()
pieLayer.path = pieBezierPath.cgPath
pieLayer.lineWidth = 5
pieLayer.strokeEnd = 1
pieLayer.fillColor = colors[i]
pieLayer.strokeColor = UIColor.white.cgColor
layer.addSublayer(pieLayer)
}
중략 ....
베지어 패스에 values 값을 계산해 호를 그리고 컬러의 값을 채울 수 있도록 하였습니다.
strokeColor를 화이트로 변경해 구분선을 표시할 수 있도록 하였습니다.
definePieChatLayer()를 사용하려면 Values, colors를 설정해 줘야겠죠?
ViewContoller에 가서 UIView를 추가하고 Class를 UIPieChatView 변경한 후 아래 코드를 추가해 줍니다.
@IBOutlet weak var piChat: UIPieChatView!
override func viewDidLoad() {
piChat.colors = [UIColor.red.cgColor, UIColor.orange.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor]
piChat.values = [0.25, 0.45, 0.10, 0.20]
}
각 순서에 맞는 %로 합이 1의 값을 가질 수 있도록 했습니다.
중간 결과를 보면 아래와 같이 나타나는 것을 볼 수 있습니다.
이제 텍스트 CATextLayer를 이용하여 각 부분에 해당하는 비율이 얼마인지 보여주도록 합니다.
private func definePieChatLayer() {
... 중략
for i in 0..<values.count {
sAngle = eAngle
eAngle = sAngle + values[i]
mAngle = sAngle + ((eAngle - sAngle) / 2) // ---- 추가
... 중략
let y:CGFloat = sin((mAngle * 2 * CGFloat.pi)) * 100 // ---- 추가
let x:CGFloat = cos((mAngle * 2 * CGFloat.pi)) * 100 // ---- 추가
let pieFontLayer = CATextLayer()
pieFontLayer.frame = CGRect(x: center.x + x, y: center.y + y, width: 0, height: 0).insetBy(dx: -50, dy: -7.5)
pieFontLayer.foregroundColor = UIColor.white.cgColor
pieFontLayer.string = "\(values[i] * 100)%"
pieFontLayer.alignmentMode = .center
pieFontLayer.fontSize = 15
pieFontLayer.font = "System" as CFTypeRef
pieFontLayer.isWrapped = true
layer.addSublayer(pieLayer)
layer.addSublayer(pieFontLayer) // ---- 추가
}
}
mAngle이라는 변수는 각 호의 중간 부분 라디언값을 계산해 텍스트를 추가해 줄 좌표를 구한 것입니다.
구한 좌표값에서 insetBy를 사용해 텍스트 프레임을 구하도록 했습니다.
각 영역에 해당하는 부분에 텍스트가 자리한 걸 확인할 수 있습니다.
이제 애니메이션을 사용해 파이가 그려지는 것처럼 보일 수 있도록 변경하겠습니다.
class UIPieChatView: UIView {
private let maskLayer = CAShapeLayer()
private let maskBezierPath = UIBezierPath()
... 중략
private func definePieChatLayer() {
...
maskBezierPath.removeAllPoints()
maskBezierPath.addArc(withCenter: center, radius: 75, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
maskLayer.path = maskBezierPath.cgPath
maskLayer.lineWidth = 150
maskLayer.strokeEnd = 0
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
layer.mask = maskLayer
}
마스크로 사용할 레이어와 베지어 패스를 선언하고 호의 반지름을 75로 설정합니다.
파이의 반지름이 150이어서 마스킹할 호를 75로 설정 후 라인 굵기를 150으로 선언해서 굵은 선이 파이를 채운다고 보면 됩니다.
func phiAnimate(duration: CFTimeInterval) {
maskLayer.strokeEnd = 0
maskLayer.removeAnimation(forKey: "maskAnimation")
pieAnimation.keyPath = "strokeEnd"
pieAnimation.duration = duration
pieAnimation.toValue = 1
pieAnimation.fillMode = .forwards
pieAnimation.isRemovedOnCompletion = false
maskLayer.add(pieAnimation, forKey: "maskAnimation")
}
뷰 컨트롤러에서 phiAnimate()를 호출해주도록 합니다.