在swift中创建饼图

时间:2015-11-26 17:41:25

标签: swift initialization pie-chart init

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

我喜欢它是@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函数中?

提前致谢!

3 个答案:

答案 0 :(得分:3)

您可以创建一个包含数据的便捷初始化,但这只有在您从代码创建视图时才有用。如果您的视图已添加到故事板中,您将需要一种在创建视图后设置数据的方法。

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

拥有包含数据的视图属性是很好的。视图应负责重绘自身,因此请为didSet属性定义data并在那里调用setNeedsDisplay()

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

然后您可以简单地设置数据,饼图将重绘:

pieChart.data = pieData

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

请注意setNeedsDisplay只设置一个标志,稍后将绘制视图。多次调用setNeedsDisplay不会导致您的视图重绘多次,因此您可以执行以下操作:

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

并且pieChart只会重绘一次。

答案 1 :(得分:3)

不,如果您已将此数据添加到情节提要中的场景(因为将调用init),则无法在init(coder:)方法中设置数据。

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

override func viewDidLoad() {
    super.viewDidLoad()

    pieChart.dataPoints = ...
}

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

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

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

enter image description here

而且,正如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() }
    }

    ...
}

答案 2 :(得分:2)

如果有人再次查看这个问题,我想发布我已完成的代码,以便它对其他人有用。这是:

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++
        }



    }
}

这将创建一个饼图,如下所示:

[The finished chart[1]

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

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
}