始点・終点の丸い円グラフを作ってみた - Swift編

さて今回はiOSのヘルスケアアプリで見かけるような始点・終点を丸くした円グラフを作ってみました。
そうこんなやつです。

![chartSampleForSwift.gif](https://qiita-image-store.s3.amazonaws.com/0/362719/5d759d86-41ea-b60c-a0ca-f7fc9a962735.gif)


環境：swift 4.2、Xcode 10.1

#円グラフを描画するUIViewの作成
まず円グラフを描画するUIViewを作成します。
円グラフの描画はCAShapeLayerとUIBezierPathを使用して行います。

```swift:ChartView.swift
import UIKit

class ChartView:UIView {
    let caShapeLayerForBase:CAShapeLayer = CAShapeLayer.init()
    let caShapeLayerForValue:CAShapeLayer = CAShapeLayer.init()
    
    func drawChart(rate:Double){
        //グラフを表示
        drawBaseChart()
        drawValueChart(rate: rate)
        
        //グラフをアニメーションで表示
        let caBasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        caBasicAnimation.duration = 2.0
        caBasicAnimation.fromValue = 0.0
        caBasicAnimation.toValue = 1.0
        caShapeLayerForValue.add(caBasicAnimation, forKey: "chartAnimation")
    }
    
    /**
     円グラフの軸となる円を表示
     */
    private func drawBaseChart(){
        let shapeFrame = CGRect.init(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
        caShapeLayerForBase.frame = shapeFrame
        caShapeLayerForBase.strokeColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0.4, alpha: 1.0).cgColor
        caShapeLayerForBase.fillColor = UIColor.clear.cgColor
        caShapeLayerForBase.lineWidth = 50
        caShapeLayerForBase.lineCap = .round
        
        let startAngle:CGFloat = CGFloat(0.0)
        let endAngle:CGFloat = CGFloat(Double.pi * 2.0)
        
        caShapeLayerForBase.path = UIBezierPath.init(arcCenter: CGPoint.init(x: shapeFrame.size.width / 2.0, y: shapeFrame.size.height / 2.0),radius: shapeFrame.size.width / 2.0,startAngle: startAngle,endAngle: endAngle,clockwise: true).cgPath
        self.layer.addSublayer(caShapeLayerForBase)
    }
    
    /**
     円グラフの値を示す円(半円)を表示
     @param rate 円グラフの値の%値
     */
    private func drawValueChart(rate:Double){
        //CAShareLayerを描く大きさを定義
        let shapeFrame = CGRect.init(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
        caShapeLayerForValue.frame = shapeFrame
        
        //CAShareLayerのデザインを定義
        caShapeLayerForValue.strokeColor = UIColor(displayP3Red: 1, green: 0.4, blue: 0.4, alpha: 1).cgColor
        caShapeLayerForValue.fillColor = UIColor.clear.cgColor
        caShapeLayerForValue.lineWidth = 50
        caShapeLayerForValue.lineCap = .round
        
        //開始位置を時計の0時の位置にする
        let startAngle:CGFloat = CGFloat(-1 * Double.pi / 2.0)
        
        //終了位置を時計の0時起点で引数渡しされた割合の位置にする
        let endAngle :CGFloat = CGFloat(rate / 100 * Double.pi * 2.0 - (Double.pi / 2.0))
        
        //UIBezierPathを使用して半円を定義
        caShapeLayerForValue.path = UIBezierPath.init(arcCenter: CGPoint.init(x: shapeFrame.size.width / 2.0, y: shapeFrame.size.height / 2.0),radius: shapeFrame.size.width / 2.0,startAngle: startAngle,endAngle: endAngle,clockwise: true).cgPath
        self.layer.addSublayer(caShapeLayerForValue)
    }
}
```

##解説
###CAShapeLayerの設定
まずCAShapeLayerのframeで大きさを定義します。今回はUIViewと同じ大きさにしておきます。

```swift
let caShapeLayerForValue:CAShapeLayer = CAShapeLayer.init()
---------- (中略) -----------
//CAShareLayerを描く大きさを定義
let shapeFrame = CGRect.init(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
caShapeLayerForValue.frame = shapeFrame
```

続いて円のデザイン設定を行います。

```swift
caShapeLayerForValue.strokeColor = UIColor(displayP3Red: 1, green: 0.4, blue: 0.4, alpha: 1).cgColor //円の線の色
caShapeLayerForValue.fillColor = UIColor.clear.cgColor //円の中の色->透明に設定して線しか表示されないようにする
caShapeLayerForValue.lineWidth = 50 //線の太さ
caShapeLayerForValue.lineCap = .round //線の始点・終点が丸くなる様に指定
```

今回のテーマである円グラフの始点と終点を丸くする設定はCAShapeLayerのlineCapプロパティに`CAShapeLayerLineCap.round`を指定するだけで実現できちゃいました。

その後、CAShareLayerのpathに円(半円)を描画するpathを渡します。
円(半円)を描画するpathはUIBezierPathを使用して作成します。

```swift
//UIBezierPathを使用して半円を定義
caShapeLayerForValue.path = UIBezierPath.init(arcCenter: CGPoint.init(x: shapeFrame.size.width / 2.0, y: shapeFrame.size.height / 2.0),radius: shapeFrame.size.width / 2.0,startAngle: startAngle,endAngle: endAngle,clockwise: true).cgPath
```

arcCenterに円(半円)の中心点となる座標を、radiusに円(半円)の半径を指定します。
重要なのは円(半円)の開始位置を示すstartAngleと終了位置を示すendAngleです。
startAngleは0を渡すと時計の3時の位置を指します。
そしてDouble.pi * 2で一周する(360度回る)様になっています。
その為、時計の0時から開始させたい時は1/4円ほどマイナス方向へ動かすので`CGFloat(-1 * Double.pi / 2.0)`を指定します。
終了位置は円1周(`Double.pi * 2`)の何%の位置かを示しますので`rate / 100 * Double.pi * 2.0`で表現できます(rateは%値)。時計の0時からの位置で示す場合は開始位置同様に1/4円ほどマイナス方向へ動かし`CGFloat(rate / 100 * Double.pi * 2.0 - (Double.pi / 2.0))`とします。
最後のclockwiseはtrueを指定すると時計周り、falseを指定すると反時計周りになります。

###円グラフをアニメーションで動かす
CABasicAnimationを使用すると円(半円)の描画をアニメーションで行うことができます。

```swift
//グラフをアニメーションで表示
let caBasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
caBasicAnimation.duration = 2.0
caBasicAnimation.fromValue = 0.0
caBasicAnimation.toValue = 1.0
caShapeLayerForValue.add(caBasicAnimation, forKey: "chartAnimation")
```

CABasicAnimationのkeyPathにはCAShapeLayerのstrokeEndプロパティを指定します。
durationにアニメーションを実行する時間を秒単位で指定します。
fromValueとtoValueはアニメーション開始時と終了時のCAShapeLayerのstrokeEndの値を指定しますが、今回のケースでは円の線の描画を途中から始めたり途中で止めたりすることはありませんので、fromValueは常に0.0、toValueは常に1.0で問題ありません。


#最後に
グラフを表示するUIView(このサンプルではChartView)をUIViewController内に貼り付けて、円グラフを描画するメソッド(このサンプルではdrawChartメソッド)を実行すれば円グラフが描画されます。円グラフの値はメソッドの引数で%値で渡しています。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    private let textRate:UITextView = UITextView()
    private let labelRate:UILabel = UILabel()
    private let buttonDraw:UIButton = UIButton()
    private let chartView:ChartView = ChartView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textRate.layer.cornerRadius = 10
        textRate.layer.borderColor = UIColor.lightGray.cgColor
        textRate.layer.borderWidth = 0.5
        textRate.keyboardType = .numberPad
        textRate.text = "75" //とりあえずデフォル値は75%
        textRate.font = UIFont.systemFont(ofSize: 16)
        self.view.addSubview(textRate)
        
        labelRate.text = "%"
        self.view.addSubview(labelRate)
        buttonDraw.setTitle("グラフ表示", for: .normal)
        buttonDraw.setTitleColor(UIColor.blue, for: .normal)
        buttonDraw.addTarget(self, action: #selector(self.touchUpButtonDraw), for: .touchUpInside)
        self.view.addSubview(buttonDraw)
        
        self.view.addSubview(chartView)
        
        changeScreen()
    }
    
    /**
     端末の向きの変更のイベント
     */
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(
            alongsideTransition: nil,
            completion: {(UIViewControllerTransitionCoordinatorContext) in
                self.changeScreen()
        }
        )
    }
    
    override func viewWillAppear(_ animated: Bool) {
        drawChart()
    }
    
    private func changeScreen(){
        let screenSize: CGRect = UIScreen.main.bounds
        let widthValue = screenSize.width
        let heightValue = screenSize.height
        
        textRate.frame = CGRect(x: widthValue/2-170, y: 50, width: 100, height: 40)
        labelRate.frame = CGRect(x: widthValue/2-70, y: 50, width: 40, height: 40)
        buttonDraw.frame = CGRect(x: widthValue/2-30, y: 50, width: 200, height: 40)
        
        var drawWidth = widthValue * 0.8
        if (widthValue > heightValue){
            drawWidth = heightValue * 0.8
        }
        chartView.frame = CGRect(x: widthValue/2-drawWidth/2, y: 150, width: drawWidth, height: drawWidth)
        
    }

    @objc func touchUpButtonDraw(){
        drawChart()
    }
    
    /**
     グラフを表示
     */
    private func drawChart(){
        let rate = Double(textRate.text!)
        chartView.drawChart(rate: rate!)
    }
}
```

#GitHub
今回紹介したサンプルコードはGitHubで公開しています。

https://github.com/naosekig/ChartSampleForSwift

#参考文献
[円グラフの描画 - Qiita](https://qiita.com/arthur87/items/d423ffcb788c2660aa3e)
[【Swift】円形にゲージが増えるアニメーション - Qiita](https://qiita.com/uin010bm/items/ff934d070d43a4666dbc)

#関連記事
[始点・終点の丸い円グラフを作ってみた - Kotlin編](https://qiita.com/NaoSekig/items/0a835523dd02480d0749)