๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป/iOS

Core Animations ํ•™์Šตํ•˜๊ธฐ - PieGraph

reujusong 2020. 12. 11. 01:15
ํ”„๋กœ์ ํŠธ ์ค‘ ํฅ๋ฏธ๋กญ๊ฒŒ ํ•™์Šตํ•œ ๋ถ€๋ถ„์ด ์žˆ์–ด ๊ธฐ๋กํ•ด๋ณด์•˜๋‹ค.
์ฐธ๊ณ : https://www.tnoda.com/blog/2019-06-18/

 

๋ฒ ์ง€์–ด ๊ณก์„ ์ด๋ž€?

- n๊ฐœ์˜ ์ ์œผ๋กœ๋ถ€ํ„ฐ ์–ป์–ด์ง€๋Š” n-1์ฐจ ๊ณก์„ 

 

  • ์• ํ”Œ์—์„œ ์ œ๊ณตํ•˜๋Š” UIBezierPath๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ง€์ถœ์— ๋Œ€ํ•œ ํŒŒ์ด์ฐจํŠธ๋ฅผ ๊ทธ๋ ค๋ณด๊ธฐ๋กœ ํ•˜์˜€๋‹ค.
  • ์žํŒ๊ธฐ ๊ตฌํ˜„ ํ”„๋กœ์ ํŠธ ๋•Œ ํ™œ์šฉํ–ˆ๋˜ ๋ฐฉ๋ฒ•๋ณด๋‹ค๋Š”, ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜์—ฌ activeํ•œ UI๋ฅผ ๋‚˜ํƒ€๋‚ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

VC์—์„œ animate ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    reportPieChartView.slices = setupSlices()
    reportPieChartView.animateChart()
}

 

  • viewDidAppear ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€์—์„œ, ํ™”๋ฉด์ด ๋ณด์—ฌ์งˆ ๋•Œ๋งˆ๋‹ค slice(์ฐจํŠธ์— ํ‘œํ˜„ํ•  ๋ชจ๋ธ)์„ setupํ•˜๊ณ  animateChart ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

 

func animateChart() {
    sliceIndex, // ํ˜„์žฌ ๊ทธ๋ฆฌ๊ณ ์ž ํ•˜๋Š” slice์˜ index
    currentPercent, // radian์œผ๋กœ ๋ฐ”๊ฟ”์ค„ percentage
    superView layer์˜ sublayers,
    ์ด์ „์— ๋‚จ์•„์žˆ๋˜ percentageLabels ์ดˆ๊ธฐํ™”

    if sliceIndex < slices.count {
        addSlice(firstSlice)
        addLabel(firstSlice)
    }
}

 

  • ํ›„์—, CAAnimationDelegate์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋๋‚ฌ์„ ๋•Œ ์—ฐ์‡„์ ์œผ๋กœ layer๋ฅผ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๋„๋ก ์ฒซ๋ฒˆ์งธ slice๋งŒ addํ•ด์ค€๋‹ค.

 

    // ๊ฐ๊ฐ์˜ ํผ์„ผํŠธ์™€ ์ƒ‰์ด ๋‹ด๊ธด ์Šฌ๋ผ์ด์Šค๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ฃผ์ž…ํ•˜์—ฌ ๊ทธ๋ฆผ
    private func addSlice(_ slice: Slice) {
        // strikeEndํ‚ค๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ.
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        // strokeEnd์˜ ๊ฒฝ์šฐ value ๋ฒ”์œ„๋Š” 0~1๊นŒ์ง€. ๋ชจ๋“  ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜
        animation.fromValue = 0
        animation.toValue = 1
        // ๊ฐ ์Šฌ๋ผ์ด์Šค์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๊ฐ„์„ ๋ฐ›์•„์˜ด. ์ „์ฒด durationd์—์„œ percentage์— ํ•ด๋‹นํ•˜๋Š” ์‹œ๊ฐ„
        animation.duration = getDuration(slice)
        // CAMediaTimingFunction.linear -> ์ผ์ •ํ•œ ์†๋„
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
        animation.delegate = self 

 

  • CABasicAnimation์˜ keypath๋ฅผ  strokeEnd๋กœ ํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ƒ์„ฑํ•ด์ค€๋‹ค.
  • CABasicAnimation์€ ์ž์ฃผ ์“ฐ๋˜ animate ํ•จ์ˆ˜๋ณด๋‹ค ๋‹ค์–‘ํ•œ ์˜ต์…˜์„ ์ค„ ์ˆ˜ ์žˆ๊ณ , ์„ค์ •ํ•œ strokeEnd ์ด์™ธ์—๋„ ๋‹ค์–‘ํ•œ keyPath๊ฐ€ ์กด์žฌํ•œ๋‹ค
    • Opacity : ํˆฌ๋ช…๋„์— ๋Œ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜
    • backgroundColor: ๋ฐฐ๊ฒฝ์ƒ‰์— ๋Œ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜
      • value ๊ฐ’์„ CGColor ํƒ€์ž…์œผ๋กœ ์ค€๋‹ค.
animation.fromValue = NSColor.red.cgColor
animation.toValue = NSColor.blue.cgColor
    • position: value๋ฅผ ์œ„์น˜ ๋ฐฐ์—ด๋กœ ๋ฐ›๋Š” ์œ„์น˜ ๋ณ€ํ™”์— ๋Œ€ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ฐธ๊ณ : ์• ํ”Œ ๊ณต์‹๋ฌธ์„œ)
  • ๋˜ํ•œ, ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์ข…๋ฃŒ ์‹œ์ ์— ํ˜ธ์ถœํ•  ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด delegate๋ฅผ ์ฑ„ํƒํ•œ๋‹ค.

 

        let canvasWidth = superView.frame.width
        let path = UIBezierPath(arcCenter: superView.center,
                                radius: canvasWidth * 3 / 8,
                                // 0 ๋ผ๋””์•ˆ๋ถ€ํ„ฐ ํผ์„ผํŠธ์— ํ•ด๋‹นํ•˜๋Š” ๋งŒํผ์˜ ๋ผ๋””์•ˆ๊นŒ์ง€ path ์ƒ์„ฑ
                                startAngle: percentToRadian(currentPercent),
                                endAngle: percentToRadian(currentPercent + slice.percent - 0.000001),
                                clockwise: true)

        // ๋‹ค๊ฐํ˜•์„ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ๋ ˆ์ด์–ด ์ƒ์„ฑ
        let sliceLayer = CAShapeLayer()
        // path ๋“ฑ๋ก
        sliceLayer.path = path.cgPath
        // ์—ฌ๊ธฐ์„œ ์ฑ„์›Œ์ง€๋Š” ์ปฌ๋Ÿฌ๋Š” path์˜ ์‹œ์ž‘๊ณผ ๋์ ์„ ์ด์–ด ๋งŒ๋“  ๋ฒ”์œ„
        sliceLayer.fillColor = nil
        sliceLayer.strokeColor = UIColor(named: slice.category.imageName + "-color")?.cgColor
        // ์› ์•ˆ์„ ์ฑ„์šฐ๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๋ผ์ธ์„ ๋‘๊ป๊ฒŒ ๊ทธ๋ฆผ
        sliceLayer.lineWidth = canvasWidth * 2 / 8
        // layer๋ฅผ ๊ทธ๋ฆฌ๋Š” ๋ฒ”์œ„. 1์ด ๋งฅ์‹œ๋ฉˆ์ด๊ณ  ๊ทธ๊ฒƒ๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ ๋น„์œจ๋งŒํผ๋งŒ ๊ทธ๋ ค์ง
        sliceLayer.strokeEnd = 1
        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋“ฑ๋ก
        sliceLayer.add(animation, forKey: animation.keyPath)

        // ๋งŒ๋“  ๋ ˆ์ด์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด์คŒ
        superView.layer.addSublayer(sliceLayer)
    }

 

  • UIBezierPath๋ฅผ ํ†ตํ•ด startAngle, endAngle์— ๋Œ€ํ•œ ํ˜ธ๋ฅผ ๊ทธ๋ ค์ค€๋‹ค.
  • ๋‹ค๊ฐํ˜• layer๋กœ CAShapeLayer๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š”๋ฐ.. ๊ทธ ์ „์— layer์— ๋Œ€ํ•ด ์กฐ๊ธˆ ์•Œ์•„๋ณด๋ฉด
  • CALayer๋Š” ์‹ค์ œ๋กœ UIView์— ์†ํ•˜๋ฉฐ UIView๋ฅผ ์ง€์›ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ๊ฐ ๋ทฐ๋งˆ๋‹ค ๋ฃจํŠธ layer๋Š” ํ•˜๋‚˜์”ฉ ์กด์žฌํ•˜๊ณ  ์ด ๋ฃจํŠธ layer๋Š” ๊ฐ๊ฐ Sublayer๋“ค์„ ๊ฐ–๋Š”๋‹ค. ์ฐธ๊ณ 
  • view ๋‚ด๋ถ€(์•„๋ž˜?)์—์„œ core animation, ๊ทธ๋ž˜ํ”ฝ๋“ฑ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • ์‹ค์ œ๋กœ ๋ทฐ ๋””๋ฒ„๊ฑฐ๋ฅผ ์ฐ์–ด๋ณด๋‹ˆ ๋ ˆ์ด๋ธ” ๋ทฐ์ฒ˜๋Ÿผ ์ถ”๊ฐ€๋˜์ง€ ์•Š๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•ด๋‹น ๋ทฐ์˜ ๋ ˆ์ด์–ด์— ๋”ฑ ๋ถ™์–ด ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค!

 

 

  • ๋‹ค์Œ, ์ƒ์„ฑํ•œ ๋ ˆ์ด์–ด์˜ path์— ์ƒ์„ฑํ•œ ํ˜ธ๋ฅผ ๋“ฑ๋กํ•˜๊ณ , color, width ๋“ฑ์„ ์„ค์ •ํ•œ๋‹ค.
  • ๋งˆ์ง€๋ง‰์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  sublayer๋กœ ๋“ฑ๋ก์„ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

 

๋‹ค์Œ ์Šฌ๋ผ์ด์Šค๋Š” ์–ด๋–ป๊ฒŒ ๊ทธ๋ฆฌ์ง€?

  • ์•ž์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด, ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ข…๋ฃŒ๋˜๋ฉด ์ฑ„ํƒ๋œ CAAnimationDelegate์— ์˜ํ•ด animationDidStop ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.
  • ์ด ๋ฉ”์†Œ๋“œ์—์„œ ๋‹ค์Œ slice์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ์ž‘์—…์„ ์—ฐ์‡„์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค.
extension ReportPieChartView: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        if flag {
            currentPercent += slices[sliceIndex].percent
            sliceIndex += 1
            if sliceIndex < slices.count {
                let nextSlice = slices[sliceIndex]
                addLabel(nextSlice)
                addSlice(nextSlice)
            }
        }
    }
}

 


percentage ๋ ˆ์ด๋ธ” ํ‘œ์‹œ

  • addSlice์™€ ๋‹ฌ๋ฆฌ, addLabel์€ ๋‹น์—ฐํ•˜์ง€๋งŒ ๋ ˆ์ด์–ด๊ฐ€ ์•„๋‹Œ ๋ทฐ๋ฅผ ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  • ์ถ”๊ฐ€ํ•  ๋ ˆ์ด๋ธ”์˜ ์œ„์น˜๋ฅผ ์ฐพ๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค
    • startAngle๊ณผ endAngle์˜ ์ค‘๊ฐ„ ์ง€์  ๊นŒ์ง€ UIBezierPath์„ ํ†ตํ•ด ํ˜ธ ๋ชจ์–‘์˜ path๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    • path๋ฅผ ๋‹ซ๊ณ  currentPoint๋ฅผ ์ฐพ์œผ๋ฉด ๊ทธ ๊ณณ์ด ๋ ˆ์ด๋ธ”์ด center๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ํ•˜๊ฒŒ ๋  ๋ฐฉํ–ฅ์ด๋‹ค.
    • UILabel์„ ์ƒ์„ฑํ•˜๊ณ  text๋ฅผ ์„ค์ •ํ•˜๊ณ  constraint๋ฅผ ์žก์•„์ค€๋‹ค.
    • ๋ ˆ์ด๋ธ”์ด ์œ„์น˜ํ•˜๊ฒŒ ๋  point๋ฅผ ์•Œ๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ๊ฐ๊ฐ์˜ x, y ์ขŒํ‘œ๋กœ๋ถ€ํ„ฐ center๋ฅผ ๋นผ์ค€ ๊ฐ’์„ centerAnchor๋กœ ์žก์•„์ฃผ๋ฉด ๋œ๋‹ค.

 

 


๊ฒฐ๊ณผ

 

 

 

  • ๋ ˆ์ด๋ธ”์— ํ•ญ๋ชฉ๋ช…๋„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.
  • ๋ ˆ์ด์–ด์™€ Core Animation์— ๋Œ€ํ•œ ํ•™์Šต์ด ๋” ํ•„์š”ํ•˜๋‹ค... 

 

 

ํ•˜๊ณ ์‹ถ์€๊ฒƒ : ์—”๋”ฉ ํฌ๋ ˆ๋”ง ์ฝ”์–ด ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๊ตฌํ˜„ ํ•ด๋ณด๊ธฐ ใ…Žใ……ใ…Ž


๊ธ€ ์ข€ ์ž์ฃผ ์จ์•ผ๊ฒ ๋‹ค ์œ ๋ น๋ธ”๋กœ๊ทธ๊ฐ€ ๋˜์–ด ๋ฒ„๋ ธ๋‹ค ํ—ˆํ—ˆ

๊ทธ๋‚˜์ €๋‚˜ ์š” ํ…œํ”Œ๋ฆฟ ๋งˆํฌ๋‹ค์šด ์ž‘์„ฑ์ด ๋„ˆ๋ฌด ๋ณ„๋กœ๋‹ค.. ๋‹ค๋ฅธ ํ…œํ”Œ๋ฆฟ ์ฐพ์•„๋ด์•ผ๊ฒ ๋Š”๊ฑธ