ios

[ios - Swift] Phi Chart View (CALayer)

POKY_0908 2021. 1. 12. 10:18

 ()

 

이번 글에서는 지난번에 배운 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()를 호출해주도록 합니다.