在 swift 中创建饼图

Creating a pie chart in swift

我正在尝试在 swift 中创建一个饼图,我想从头开始创建代码而不是使用第 3 方扩展。

我喜欢它是@IBDesignable 的想法,所以我从这个开始:

import Foundation
import UIKit

@IBDesignable class PieChart: UIView {

  var data:  Dictionary<String,Int>?

  required init(coder aDecoder: NSCoder) {
    super.init(coder:aDecoder)!
    self.contentMode = .Redraw
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.clearColor()
    self.contentMode = .Redraw
  }

  override fun drawRect(rect: CGRect) {
    // draw the chart in here
  }

}

我不确定的是如何最好地将数据放入图表中。我应该有这样的东西吗:

@IBOutlet weak var pieChart: PieChart!
override func viewDidLoad() {
    pieChart.data = pieData
    pieChart.setNeedsDisplay()
}

或者有更好的方法吗?想必是没有办法把数据包含在init函数中吧?

提前致谢!

您可以创建一个包含数据的便利 init,但这只有在您从代码创建视图时才有用。如果您的视图被添加到情节提要中,您将需要一种在创建视图后设置数据的方法。

最好查看标准 UI 元素(如 UIButton)以获取设计线索。您可以更改 UIButton 的属性,它会更新而无需调用 myButton.setNeedsDisplay(),因此您应该将饼图设计为以相同的方式工作。

拥有一个保存数据的视图 属性 很好。该视图应负责重绘自身,因此为您的 data 属性 定义 didSet 并在那里调用 setNeedsDisplay()

var data:  Dictionary<String,Int>? {
    didSet {
        // Data changed.  Redraw the view.
        self.setNeedsDisplay()
    }
}

然后你可以简单的设置数据,饼图会重新绘制:

pieChart.data = pieData

您可以将其扩展到饼图上的其他属性。例如,您可能想要更改背景颜色。您还可以为 属性 定义 didSet 并调用 setNeedsDisplay.

注意setNeedsDisplay只是设置了一个flag,view会在后面绘制。多次调用 setNeedsDisplay 不会导致您的视图多次重绘,因此您可以执行以下操作:

pieChart.data = pieData
pieChart.backgroundColor = .redColor()
pieChart.draw3D = true  // draw the pie chart in 3D

饼图只会重绘一次。

不行,如果你已经把它添加到故事板的场景中,你不能在init方法中设置数据(因为init(coder:)会被调用)。

所以,是的,您可以在 viewDidLoad 中填充饼图的数据。

override func viewDidLoad() {
    super.viewDidLoad()

    pieChart.dataPoints = ...
}

但是,因为这个 PieChartIBDesignable,这意味着您可能想在 IB 中查看饼图的再现。因此,您可以在 PieChart class 中实现 prepareForInterfaceBuilder,提供一些示例数据:

override public func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    dataPoints = ...
}

这样您现在就可以在 Interface Builder 中享受可设计的视图(例如,查看预览;可以显示其他可检查的属性)。预览图是我们的示例数据,不是运行时会显示的数据,但可能足以欣赏整体设计:

而且,正如 vacawama 所说,您想要将 setNeedsDisplay 移到 属性 的 didSet 观察器中。

public class PieChart: UIView {

    public var dataPoints: [DataPoint]? {          // use whatever type that makes sense for your app, though I'd suggest an array (which is ordered) rather than a dictionary (which isn't)
        didSet { setNeedsDisplay() }
    }

    @IBInspectable public var lineWidth: CGFloat = 2 {
        didSet { setNeedsDisplay() }
    }

    ...
}

为了防止有人再次查看这个问题,我想 post 我完成的代码,以便它对其他人有用。这是:

import Foundation
import UIKit

@IBDesignable class PieChart: UIView {
    var dataPoints: Dictionary<String,Double> = ["Alpha":1,"Beta":2,"Charlie":3,"Delta":4,"Echo":2.5,"Foxtrot":1.4] {
        didSet { setNeedsDisplay() }
    }
    @IBInspectable var lineWidth: CGFloat = 1.0 {
        didSet { setNeedsDisplay()
        }
    }
    @IBInspectable var lineColor: UIColor = uicolor_normal {
        didSet { setNeedsDisplay() }
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder:aDecoder)!
        self.contentMode = .Redraw
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.clearColor()
        self.contentMode = .Redraw
    }

    override func drawRect(rect: CGRect) {


        // set font for labels
        let fieldColor: UIColor = UIColor.darkGrayColor()
        let fieldFont = uifont_piechartkey
        var fieldAttributes: NSDictionary = [
            NSForegroundColorAttributeName: fieldColor,
            NSFontAttributeName: fieldFont!
        ]

        // get the graphics context and prepare an inset box for the pie
        let ctx = UIGraphicsGetCurrentContext()
        let margin: CGFloat = lineWidth
        let box0 = CGRectInset(self.bounds, margin, margin)
        let keyHeight = CGFloat( ceil( Double(dataPoints.count) / 3.0) * 24 ) + 16
        let side : CGFloat = min(box0.width, box0.height-keyHeight)
        let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side-keyHeight)/2,side,side)
        let radius : CGFloat = min(box.width, box.height)/2.0


        // converts percentages to radians for drawing the segment
        func percent_to_rad(p: Double) -> CGFloat {
            let rad = CGFloat(p * 0.02 * M_PI)
            return rad
        }

        // draws a segment
        func draw_arc(start: CGFloat, end: CGFloat, color: CGColor) {
            CGContextBeginPath(ctx)
            CGContextMoveToPoint(ctx, box.midX, box.midY)
            CGContextSetFillColorWithColor(ctx, color)
            CGContextAddArc(ctx,box.midX,box.midY,radius-lineWidth/2,start,end,0)
            CGContextClosePath(ctx)
            CGContextFillPath(ctx)
        }
        // draws a key item
        func draw_key(keyName: String, keyValue: Double, color: CGColor, keyX: CGFloat, keyY: CGFloat) {
            CGContextBeginPath(ctx)
            CGContextMoveToPoint(ctx, keyX, keyY)
            CGContextSetFillColorWithColor(ctx, color)
            CGContextAddArc(ctx,keyX,keyY,8,0,CGFloat(2 * M_PI),0)
            CGContextClosePath(ctx)
            CGContextFillPath(ctx)
            keyName.drawInRect(CGRectMake(keyX + 12,keyY-8,self.bounds.width/3,16),withAttributes: fieldAttributes as? [String : AnyObject])
        }


        let total =  Double(dataPoints.values.reduce(0, combine: +)) // the total of all values
        // convert dictionary to sorted touples
        let dataPointsArray = dictionary_to_sorted_array(dataPoints)

        // now sort the dictionary into an Array
        var start = -CGFloat(M_PI_2) // start at 0 degrees, not 90
        var end: CGFloat
        var i = 0

        // draw all segments
        for dataPoint in dataPointsArray {
            end = percent_to_rad(Double( (dataPoint.value)/total) * 100 )+start
            draw_arc(start,end:end,color: uicolors_chart[i%uicolors_chart.count].CGColor)
            start = end
            i++
        }
        // the key
        var keyX = self.bounds.minX + 8
        var keyY = self.bounds.height - keyHeight + 32
            i = 0
        for dataPoint in dataPointsArray {
            draw_key(dataPoint.key, keyValue: dataPoint.value, color: uicolors_chart[i%uicolors_chart.count].CGColor, keyX: keyX, keyY: keyY)
            if((i+1)%3 == 0) {
                keyX = self.bounds.minX + 8
                keyY += 24
            } else {
                keyX += self.bounds.width / 3
            }
            i++
        }



    }
}

这将创建一个饼图,看起来像这样:

[

您需要的其他代码是颜色数组:

let uicolor_chart_1 = UIColor.init(red: 0.0/255, green:153.0/255, blue:255.0/255, alpha:1.0)  //16b
let uicolor_chart_2 = UIColor.init(red: 0.0/255, green:200.0/255, blue:120.0/255, alpha:1.0)
let uicolor_chart_3 = UIColor.init(red: 140.0/255, green:220.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_4 = UIColor.init(red: 255.0/255, green:240.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_5 = UIColor.init(red: 255.0/255, green:180.0/255, blue:60.0/255, alpha:1.0)
let uicolor_chart_6 = UIColor.init(red: 235.0/255, green:60.0/255, blue:150.0/255, alpha:1.0)
let uicolors_chart : [UIColor] = [uicolor_chart_1,uicolor_chart_2,uicolor_chart_3,uicolor_chart_4,uicolor_chart_5,uicolor_chart_6]

以及将字典转换为数组的代码:

func dictionary_to_sorted_array(dict:Dictionary<String,Double>) ->Array<(key:String,value:Double)> {
    var tuples: Array<(key:String,value:Double)> = Array()
    let sortedKeys = (dict as NSDictionary).keysSortedByValueUsingSelector("compare:")
    for key in sortedKeys {
        tuples.append((key:key as! String,value:dict[key as! String]!))
    }
    return tuples
}