Help us understand the problem. What is going on with this article?

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

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

chartSampleForSwift.gif

環境:swift 4.2、Xcode 10.1

円グラフを描画するUIViewの作成

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

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と同じ大きさにしておきます。

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

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

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を使用して作成します。

//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を使用すると円(半円)の描画をアニメーションで行うことができます。

//グラフをアニメーションで表示
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メソッド)を実行すれば円グラフが描画されます。円グラフの値はメソッドの引数で%値で渡しています。

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
【Swift】円形にゲージが増えるアニメーション - Qiita

関連記事

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした