Swift 円を描くストロークアニメーション その2

目的の動作

実装コード

ViewController.swift

class ViewController: UIViewController {
    // アニメーション
    var animation: CAKeyframeAnimation {
        // ストロークアニメーションを指定してインスタンス化
        let animation = CAKeyframeAnimation(keyPath: "strokeEnd")
        // デリゲート設定
        animation.delegate = self
        // 今回は0°~360°の円を描くのでvaluesの0は0°になります。
        // valuesの1は360°になります。
        animation.values = [0, 1]
        // アニメーションのスピード設定
        animation.speed = 1
        return animation
    }
    
    /// ストロークの色ごとにShapeLayerを分けてこの配列で保持する
    private var shapeLayers: [CAShapeLayer] = []
    /// アニメーション対象の配列index
    private var nextAnimationIndex: Int = 0
    private var space: CGFloat = 4
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black
        drawCircle()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        startAnimation()
    }
    
    /// 円を描くlayer設定
    private func drawCircle() {
        let cgColors = [
            UIColor.blue,
            UIColor.red,
            UIColor.green,
            UIColor.orange
        ].map { $0.cgColor }
        
        cgColors.enumerated().forEach { index, color in
            // 角度
            let angle = CGFloat(360) / CGFloat(cgColors.count)
            // 円のパスを作成
            let bezierPath = UIBezierPath(
                // 円の中心を画面の中心にする
                arcCenter: view.center,
                // 円の半径を100に設定
                radius: 100,
                // 開始角度を0°に設定 (3時の方向が0°になります)
                startAngle: radian(angle: (angle * CGFloat(index)) + space),
                // 終了角度を360°に設定
                endAngle: radian(angle: angle * CGFloat(index + 1) ),
                // true: 時計回り, false: 反時計回り
                clockwise: true
            )
            let shapeLayer = CAShapeLayer()
            // 線の太さ
            shapeLayer.lineWidth = 3
            // 線のカラー
            shapeLayer.strokeColor = color
            // 円の内側のカラー
            shapeLayer.fillColor = UIColor.clear.cgColor
            // 作成したパスを設定する
            shapeLayer.path = bezierPath.cgPath
            // アニメーションを開始するまで非表示にしておく
            shapeLayer.isHidden = true
            shapeLayer.shadowColor = UIColor.white.cgColor
            shapeLayer.shadowRadius = 4
            shapeLayer.shadowOpacity = 1
            shapeLayer.shadowOffset = .zero
            // インスタンス変数の配列に格納しておく
            shapeLayers.append(shapeLayer)
            // 円を描くlayerを設定する
            view.layer.addSublayer(shapeLayer)
        }
    }
    
    /// 角度をラジアンに変換
    /// - Parameter angle: 角度
    /// - Returns: ラジアン
    private func radian(angle: CGFloat) -> CGFloat {
        Double.pi / 180 * angle
    }
    
    /// アニメーションを開始する
    func startAnimation() {
        // アニメーションを行いたいレイヤーに作成したアニメーションを設定する
        let shapeLayer = shapeLayers[nextAnimationIndex]
        shapeLayer.isHidden = false
        shapeLayer.add(animation, forKey: nil)
    }
}


// MARK: - CAAnimationDelegate

extension ViewController: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        guard flag else { return }
        if shapeLayers.indices.contains(nextAnimationIndex + 1) {
            nextAnimationIndex += 1
            startAnimation()
        } else {
            nextAnimationIndex = 0
        }
    }
}

まとめ

複数のカラーで1つの円を描く実装方法の参考記事が見当たらなかったので作成してみました。
今回簡潔にするため4色の比率を当分で表示してますが、データを元に割合に応じて指定することもできます。

補足

複数のカラーで円を描く際の最低限のコードも載せておきます。
ほとんど変わりませんが上記のコードにはちょっとカッコよく見せるために必須ではないコードも追加したので最低限のコードだけでいい!という場合は下記を参考にしてください!

実装コード

ViewController.swift

class ViewController: UIViewController {
    // アニメーション
    var animation: CAKeyframeAnimation {
        // ストロークアニメーションを指定してインスタンス化
        let animation = CAKeyframeAnimation(keyPath: "strokeEnd")
        // デリゲート設定
        animation.delegate = self
        // 今回は0°~360°の円を描くのでvaluesの0は0°になります。
        // valuesの1は360°になります。
        animation.values = [0, 1]
        // アニメーションのスピード設定
        animation.speed = 1
        return animatio
    }
    
    /// ストロークの色ごとにShapeLayerを分けてこの配列で保持する
    private var shapeLayers: [CAShapeLayer] = []
    /// アニメーション対象の配列index
    private var nextAnimationIndex: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        drawCircle()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        startAnimation()
    }
    
    /// 円を描くlayer設定
    private func drawCircle() {
        let cgColors = [
            UIColor.blue,
            UIColor.red,
            UIColor.green,
            UIColor.orange
        ].map { $0.cgColor }
        
        cgColors.enumerated().forEach { index, color in
            // 角度
            let angle = CGFloat(360) / CGFloat(cgColors.count)
            // 円のパスを作成
            let bezierPath = UIBezierPath(
                // 円の中心を画面の中心にする
                arcCenter: view.center,
                // 円の半径を100に設定
                radius: 100,
                // 開始角度を0°に設定 (3時の方向が0°になります)
                startAngle: radian(angle: angle * CGFloat(index)),
                // 終了角度を360°に設定
                endAngle: radian(angle: angle * CGFloat(index + 1) ),
                // true: 時計回り, false: 反時計回り
                clockwise: true
            )
            let shapeLayer = CAShapeLayer()
            // 線の太さ
            shapeLayer.lineWidth = 3
            // 線のカラー
            shapeLayer.strokeColor = color
            // 円の内側のカラー
            shapeLayer.fillColor = UIColor.clear.cgColor
            // 作成したパスを設定する
            shapeLayer.path = bezierPath.cgPath
            // アニメーションを開始するまで非表示にしておく
            shapeLayer.isHidden = true
            // インスタンス変数の配列に格納しておく
            shapeLayers.append(shapeLayer)
            // 円を描くlayerを設定する
            view.layer.addSublayer(shapeLayer)
        }
    }
    
    /// 角度をラジアンに変換
    /// - Parameter angle: 角度
    /// - Returns: ラジアン
    private func radian(angle: CGFloat) -> CGFloat {
        Double.pi / 180 * angle
    }
    
    /// アニメーションを開始する
    func startAnimation() {
        // アニメーションを行いたいレイヤーに作成したアニメーションを設定する
        let shapeLayer = shapeLayers[nextAnimationIndex]
        shapeLayer.isHidden = false
        shapeLayer.add(animation, forKey: nil)
    }
}


// MARK: - CAAnimationDelegate

extension ViewController: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        guard flag else { return }
        if shapeLayers.indices.contains(nextAnimationIndex + 1) {
            nextAnimationIndex += 1
            startAnimation()
        } else {
            nextAnimationIndex = 0
        }
    }
}