Swift — 客製化圖表 Custom Chart
火箭倒數計時了,承受不住壓力的,快跳吧。🚀
這個禮拜在好想工作室我們的 mentor —— Don 給每個人分派了不一樣的挑戰,而我這次收到的任務是「 Custom Chart 」,也有附帶個幾個條件限制:
條件:
- 實現圓餅圖與折線圖。
- 做成 Library or Framework 的形式給別人使用。
- 正常使用情況下不跑版。
起初還以為是找一個第三方或是 Cocoa Pods 找一個插件來使用,然後做出更多功能之類的還是客製化功能更多之類的,後來才發現原來是要自己從零開始畫出一個折線圖(早上還找了一堆第三方圖表來思考和測試怎麼優化 😂😂😂)
於是就正式展開了自幹圖表之旅……
事前規劃
還沒開始編寫程式的時候,應該就可以先好好思考「 我的折線圖需要什麼功能?」,於是我剛開始就思考如果是我我會想要有什麼功能,或許做不出來,也許可以使用其他方式解決,於是統整出下列功能:
折線圖:
- 希望圖表需要可以滑動的
- 折線圖可以標示出座標的 “ 點 ”
- 必須有刻度以及點上的位置
圓餅圖:
- 可以顯示每個顏色的百分比
- 每個圖可以顯示隨機顏色
- 如何畫一個圓以及一堆扇型
規劃好了大方向之後,第一個圖表 —— 折線圖,我們折線圖需要畫出點跟點之間的連線,因此我就上網 Google 如何在 Swift 中進行畫線的動作,於是找到可以使用 Swift 中的 UIBezierPath 與 CAShapeLayer 配合,來進行畫線或是畫圓的動作。
其中主要實作參考了這篇文章,大致上能夠讓我明白 UIBezierPath 與 CAShapeLayer 配合 可以做出什麼樣的效果:
【 Swift-貝賽爾曲線畫扇形、弧線、圓形、多邊形 — — UIBezierPath實現App下載時的動畫效果 】:https://www.jianshu.com/p/c5cbb5e05075
自製折線圖
前面有提到我們的折線圖可能需要一個可以滑動的 View,因此我想到了可以使用 Scroll View 來實現這個想法,我可以透過在 Scroll View 裏頭新增一個 折線圖的 View ,未來如果要實現折線圖縮放也是可以的。
var chartScrollView:UIScrollView!var chartView:UIView!
// 生成 Scroll view 以及折線圖 ViewchartView = UIView(frame: view.frame)chartView.backgroundColor = UIColor.clearchartScrollView = UIScrollView(frame: view.frame)chartScrollView.backgroundColor = UIColor.blackchartScrollView.contentSize = chartView.frame.sizechartScrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]chartScrollView.addSubview(chartView)view.addSubview(chartScrollView)
當生成完畢這兩個 View 之後,我們就開始在 chartView 上開始實現畫線,首先先宣告兩個我們會需要用到的 UIBezierPath 和 CAShapeLayer:
let linePath = UIBezierPath()let lineShapeLayer = CAShapeLayer()
因為我們畫線一開始會需要一個起始點,因此我們會用到 move(to: CGPoint) 這個方法:
linePath.move(to: CGPoint(x: xPosition, y: yPosition))
// 這邊我的 x, y 是我定義的變數,因為我想要從左下角開始
// 所以 x 預設 0 , y 預設為 view.frame.maxY
因為我們的畫線方式一定是個會重複使用的方法,因此我寫了一個 function 把他包在裡面,讓他能照著我定義的樣式連線:
func updateChart() {updateChartViewFrame()linePath.addLine(to: CGPoint(x: xPosition, y: yPosition))lineShapeLayer.path = linePath.cgPathlineShapeLayer.fillColor = UIColor.clear.cgColorlineShapeLayer.strokeColor = UIColor.green.cgColorlineShapeLayer.lineWidth = 1chartView.layer.addSublayer(lineShapeLayer)}
因為我腦海中的折線圖 x 軸座標都是固定位移,y 軸則是使用者自訂,因此我寫了一個迴圈來產生連線的節點:
let randomRange = UInt32(self.chartView.frame.height)for _ in 0...100 {xPosition += 100yPosition = CGFloat(arc4random() % randomRange)updateChart()}
當然我們的節點可能會超出我們的 View 的範圍,所以我這邊用一個簡單的方式解決,直接判斷我們 x 軸是否超出我們的 View 如果有就增加 View 的寬度:
func updateChartViewFrame() {if xPosition >= chartView.frame.maxX {chartView.frame = CGRect(x: 0, y: 0, width: xPosition + 50, height: chartView.frame.height)chartScrollView.contentSize = chartView.frame.size}}
接下來我們就可以看到類似心電圖的感覺:
前面提到,覺得折線圖可能需要加上個標記,以及座標位置可以讓能見度提升一些,因此我們可以產生一個圓形的 View 並且把中心設置為我們的當前的 x,y,並生成一個 Label 標示他們的座標位置:
func markPoint() {let circlePoint = UIView()circlePoint.frame = CGRect(x: 0, y: 0, width: 10, height: 10)circlePoint.layer.cornerRadius = circlePoint.frame.height / 2circlePoint.clipsToBounds = truecirclePoint.center = CGPoint(x: xPosition, y: yPosition)circlePoint.backgroundColor = UIColor.greenlet coordinateLabel = UILabel()coordinateLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 40)coordinateLabel.text = "(\(Int(xPosition)),\(Int(chartView.frame.height - yPosition)))"coordinateLabel.center = CGPoint(x: xPosition, y: yPosition - 20)coordinateLabel.textColor = UIColor.greenchartView.addSubview(coordinateLabel)chartView.addSubview(circlePoint)}
畫面如下:
之後再依據需求加上此圖表的 x , y 軸座標位置,就完成折線圖了。
圓餅圖
因為我們繪製圓餅圖的時候需要依照他佔有的比例來自訂他的起始點以及終點,同時也要自訂它的顏色,所以寫了一個 function 來套在繪製圓餅圖上:
func pieChart(radius:CGFloat,startAngle:CGFloat,endAngle:CGFloat,color:CGColor) {print(startAngle, endAngle)let path = UIBezierPath(arcCenter: view.center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)path.addLine(to: view.center)let layer = CAShapeLayer()layer.path = path.cgPathlayer.fillColor = colorself.view.layer.addSublayer(layer)}// 可以依照你的需求 修改 function 中的參數
並且擴展 UIColor 來產生隨機顏色:
extension UIColor {open class var randomColor:UIColor {get {let red = CGFloat(arc4random() % 256) / 255let green = CGFloat(arc4random() % 256) / 255let blue = CGFloat(arc4random() % 256) / 255return UIColor(red: red, green: green, blue: blue, alpha: 1.0)}}}
再來我們需要幾個變數來記錄我們圓餅圖的所需資訊:
var total:Double = 0 // 紀錄圓餅圖數據總和var start:CGFloat = 0 // 紀錄起始點var numberArray = [Double]() // 紀錄圓餅圖個別數據
之後像前面寫了一個懶人迴圈來產生這些數據:
// 產生亂數
for _ in 1...3 {let number = Double(arc4random() % 10) + 1numberArray.append(number)}// 計算總和for number in numberArray {total += numberprint("Number\(number)")}
之後再寫一個迴圈套用在前面產生的結果中:
for i in 0...numberArray.count - 1 {
let randomColor = UIColor.randomColorpieChart(radius: 200, startAngle: start, endAngle: start + CGFloat(2 * Double.pi / total * numberArray[i]) , color: randomColor.cgColor)// 我們使用一個圓 (2 * Double.pi) 來除以合計再乘以目前的數字,產生的結果就是我們目前數字的所佔 total 的比例。start += CGFloat(2 * Double.pi / total * numberArray[i])// 這邊的 start 必須加上之前的扇形範圍,才是正確的起始點showColorPercent(centerX: 30, centerY: 30 * (i + 1), color: randomColor, i : i)}
因為圓餅圖不容易看出每個扇形的比例,所以必須產生該顏色百分比的標籤,所以我定義了下面的方法,讓在進行上面迴圈的時候,能夠把這些數據傳進這個 function 中,產生標籤紀錄:
func showColorPercent(centerX x:Int,centerY y:Int,color:UIColor,i:Int) {let colorLabel = UILabel()colorLabel.frame = CGRect(x: 0, y: 0, width: 50, height: 30)colorLabel.center = CGPoint(x: x, y: y)colorLabel.backgroundColor = colorcolorLabel.textAlignment = NSTextAlignment.centercolorLabel.text = "\(Int(numberArray[i] / total * 100))%"view.addSubview(colorLabel)}
因為是第一次實作圖表、也有點急急忙忙地完成,如果有可以修正的更好的地方麻煩指導。
加上我真的很懶惰寫了一堆迴圈產生數據
附上 github 連結:https://github.com/JeremyXue77/Custom-Chart