核心动画中的简单饼图

Simple Pie Chart in Core Animation

我正在尝试使用 Core Animation 在我的应用程序中包含一个简单的饼图。 网上找了一篇文章复制调整,貌似和我需要的差不多。

https://github.com/tomnoda/piechart_ios

该代码引用了 Nib 文件(我不是很了解),但我可以通过编程来代替吗?我认为这是需要更改的代码行,也许我还需要添加一些其他编码:-

     required init?(coder aDecoder: NSCoder) {
         super.init(coder: aDecoder)
         let view: UIView = Bundle.main.loadNibNamed("PieChartView", owner: self, options: nil)!.first as! UIView
     addSubview(view)

let 行引用了 Nib 文件,但我怎样才能让它引用我的视图控制器呢? 这显然会导致一系列未解决的标识符错误,因为这 2 个文件没有按应有的方式链接。在 View Controller 上,我有以下内容以及许多其他渠道:-

    @IBOutlet weak var  pieChartView: PieChartView!

由于我是 Xcode 的新手,希望这个问题有一个简单的解决方法。

"I'm trying to include a simple pie chart in my app using Core Animation"

首先,从该语句中删除单词 simple。听起来不像是个混蛋,但如果您是初学者,甚至不了解 nib (xib) 中布局的元素与通过代码创建元素,那么您将有很长的路要走。

虽然您链接到 "works," 的示例有很多限制,并且采用了一些相当奇怪的方法来完成任务。例如:

  • 限制为 5 个或更少的段
  • 段值的总和必须等于 1.0
  • 它几乎没有错误检查的方式

也就是说,它可能是您开始学习的好地方。

这是相同的代码,修改为不需要 xib 文件。可以这样使用:

class ViewController: UIViewController {

    @IBOutlet var pieChartView: MyPieChartView!

    override func viewDidLoad() {
        super.viewDidLoad()

        pieChartView.slices = [
            Slice(percent: 0.4, color: UIColor.red),
            Slice(percent: 0.3, color: UIColor.blue),
            Slice(percent: 0.2, color: UIColor.purple),
            Slice(percent: 0.1, color: UIColor.green)
        ]
    }

    override func viewDidAppear(_ animated: Bool) {
        pieChartView.animateChart()
    }
}

这是MyPieChartView.swift ...

原始 PieChartView.swift 文件的第一个更改位于顶部,介于:

// MARK: Changes start here
// MARK: Changes end here

其他更改以允许 "anti-clockwise" ... 查找新 Bool var 的实例 drawClockwise

import UIKit

class MyPieChartView: UIView {

    static let ANIMATION_DURATION: CGFloat = 1.4

// MARK: Changes start here
    var canvasView: UIView!

    var label1: UILabel!
    var label2: UILabel!
    var label3: UILabel!
    var label4: UILabel!
    var label5: UILabel!

    var label1XConst: NSLayoutConstraint!
    var label2XConst: NSLayoutConstraint!
    var label3XConst: NSLayoutConstraint!
    var label4XConst: NSLayoutConstraint!
    var label5XConst: NSLayoutConstraint!

    var label1YConst: NSLayoutConstraint!
    var label2YConst: NSLayoutConstraint!
    var label3YConst: NSLayoutConstraint!
    var label4YConst: NSLayoutConstraint!
    var label5YConst: NSLayoutConstraint!

    var drawClockwise: Bool = true

    var slices: [Slice]?
    var sliceIndex: Int = 0
    var currentPercent: CGFloat = 0.0

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func commonInit() -> Void {

        if canvasView == nil {

            let container = UIView()
            addSubview(container)

            canvasView = UIView()
            container.addSubview(canvasView)

            canvasView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                canvasView.topAnchor.constraint(equalTo: container.topAnchor),
                canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
                canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
                canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
            ])

            canvasView.backgroundColor = .yellow

            label1 = UILabel()
            label2 = UILabel()
            label3 = UILabel()
            label4 = UILabel()
            label5 = UILabel()

            [label1, label2, label3, label4, label5].forEach {
                guard let v = [=12=] else { fatalError("Bad Setup!") }
                v.translatesAutoresizingMaskIntoConstraints = false
                v.textColor = .white
                v.textAlignment = .center
                addSubview(v)
            }

            label1XConst = label1.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
            label1YConst = label1.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)

            label2XConst = label2.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
            label2YConst = label2.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)

            label3XConst = label3.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
            label3YConst = label3.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)

            label4XConst = label4.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
            label4YConst = label4.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)

            label5XConst = label5.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
            label5YConst = label5.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)

            [label1XConst, label2XConst, label3XConst, label4XConst, label5XConst,
             label1YConst, label2YConst, label3YConst, label4YConst, label5YConst].forEach {
                [=12=]?.isActive = true
            }
        }

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        subviews[0].frame = bounds
    }

    // don't do this
    //override func draw(_ rect: CGRect) {
    //  subviews[0].frame = bounds
    //}
// MARK: Changes end here

    /// Get an animation duration for the passed slice.
    /// If slice share is 40%, for example, it returns 40% of total animation duration.
    ///
    /// - Parameter slice: Slice struct
    /// - Returns: Animation duration
    func getDuration(_ slice: Slice) -> CFTimeInterval {
        return CFTimeInterval(slice.percent / 1.0 * PieChartView.ANIMATION_DURATION)
    }

    /// Convert slice percent to radian.
    ///
    /// - Parameter percent: Slice percent (0.0 - 1.0).
    /// - Returns: Radian
    func percentToRadian(_ percent: CGFloat) -> CGFloat {
        //Because angle starts wtih X positive axis, add 270 degrees to rotate it to Y positive axis.
        var angle = 270 + percent * 360
        if angle >= 360 {
            angle -= 360
        }
        return angle * CGFloat.pi / 180.0
    }

    /// Add a slice CAShapeLayer to the canvas.
    ///
    /// - Parameter slice: Slice to be drawn.
    func addSlice(_ slice: Slice) {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = getDuration(slice)
        animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
        animation.delegate = self

        let canvasWidth = canvasView.frame.width
        let toPercent = currentPercent + (drawClockwise ? slice.percent : -slice.percent)
        let path = UIBezierPath(arcCenter: canvasView.center,
                                radius: canvasWidth * 3 / 8,
                                startAngle: percentToRadian(currentPercent),
                                endAngle: percentToRadian(toPercent),
                                clockwise: drawClockwise)

        let sliceLayer = CAShapeLayer()
        sliceLayer.path = path.cgPath
        sliceLayer.fillColor = nil
        sliceLayer.strokeColor = slice.color.cgColor
        sliceLayer.lineWidth = canvasWidth * 2 / 8
        sliceLayer.strokeEnd = 1
        sliceLayer.add(animation, forKey: animation.keyPath)

        canvasView.layer.addSublayer(sliceLayer)
    }

    /// Get label's center position based on from and to percentages.
    /// This is always relative to canvasView's center.
    ///
    /// - Parameters:
    ///   - fromPercent: End of previous slice.
    ///   - toPercent: End of current slice.
    /// - Returns: Center point for label.
    func getLabelCenter(_ fromPercent: CGFloat, _ toPercent: CGFloat) -> CGPoint {
        let radius = canvasView.frame.width * 3 / 8
        let labelAngle = percentToRadian((toPercent - fromPercent) / 2 + fromPercent)
        let path = UIBezierPath(arcCenter: canvasView.center,
                                radius: radius,
                                startAngle: labelAngle,
                                endAngle: labelAngle,
                                clockwise: drawClockwise)
        path.close()
        return path.currentPoint
    }

    /// Re-position and draw label such as "43%".
    ///
    /// - Parameter slice: Slice whose label is drawn.
    func addLabel(_ slice: Slice) {
        let center = canvasView.center
        let labelCenter = getLabelCenter(currentPercent, currentPercent + (drawClockwise ? slice.percent : -slice.percent))
        let xConst = [label1XConst, label2XConst, label3XConst, label4XConst, label5XConst][sliceIndex]
        let yConst = [label1YConst, label2YConst, label3YConst, label4YConst, label5YConst][sliceIndex]
        xConst?.constant = labelCenter.x - center.x
        yConst?.constant = labelCenter.y - center.y

        let label = [label1, label2, label3, label4, label5][sliceIndex]
        label?.isHidden = true
        label?.text = String(format: "%d%%", Int(slice.percent * 100))
    }

    /// Call this to start pie chart animation.
    func animateChart() {
        sliceIndex = 0
        currentPercent = 0.0
        canvasView.layer.sublayers = nil

        if slices != nil && slices!.count > 0 {
            let firstSlice = slices![0]
            addLabel(firstSlice)
            addSlice(firstSlice)
        }
    }
}

extension MyPieChartView: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        if flag {
            currentPercent += (drawClockwise ? slices![sliceIndex].percent : -slices![sliceIndex].percent)
            sliceIndex += 1
            if sliceIndex < slices!.count {
                let nextSlice = slices![sliceIndex]
                addLabel(nextSlice)
                addSlice(nextSlice)
            } else {
                //After animation is done, display all labels. Can be animated.
                for label in [label1, label2, label3, label4, label5] {
                    label?.isHidden = false
                }
            }
        }
    }
}

示例:

class ViewController: UIViewController {

    @IBOutlet var pieChartView: MyPieChartView!
    @IBOutlet var antiPieChartView: MyPieChartView!

    override func viewDidLoad() {
        super.viewDidLoad()

        pieChartView.slices = [
            Slice(percent: 0.4, color: UIColor.red),
            Slice(percent: 0.3, color: UIColor.blue),
            Slice(percent: 0.2, color: UIColor.purple),
            Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
        ]

        antiPieChartView.slices = [
            Slice(percent: 0.4, color: UIColor.red),
            Slice(percent: 0.3, color: UIColor.blue),
            Slice(percent: 0.2, color: UIColor.purple),
            Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
        ]

        // draw this pie anti-clockwise
        antiPieChartView.drawClockwise = false
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        pieChartView.animateChart()
        antiPieChartView.animateChart()
    }
}