3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2023

Day 10

カスタムCAShapeLayerクラスのAnimatableなプロパティでアニメーションを動かす

Last updated at Posted at 2023-12-09

概要

CAShapeLayerで描画されたViewをアニメーションさせたいケースがあると思います。

strokeStartstrokeEndなどのCAShapeLayerで定義しているプロパティを指定することで、プロパティ変化に応じてCABasicAnimationによるアニメーションを実行することができます。

しかし、描画する内容によっては、独自のプロパティを指定してアニメーションを実行させたい時があります。

そこで本記事では、円グラフを描画するCAShapeLayerクラスを例に、Animatableなプロパティを定義する方法について説明します。

完成イメージ

画面収録-2023-11-24-18.56.03.gif

手順

  1. カスタムCAShapeLayerクラスを作成
  2. カスタムCAShapeLayerを持つViewを作成
  3. Viewを利用

カスタムCAShapeLayerクラスを作成

final class ArcLayer: CAShapeLayer {
    var tintColor: UIColor = .black

    @objc dynamic var startAngle: CGFloat = 0.0
    @objc dynamic var endAngle: CGFloat = 0.0

    override init() {
        super.init()
    }

    override init(layer: Any) {
        if let arcLayer = layer as? ArcLayer {
            self.tintColor = arcLayer.tintColor
            self.startAngle = arcLayer.startAngle
            self.endAngle = arcLayer.endAngle
        }
        super.init(layer: layer)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func draw(in ctx: CGContext) {
        ctx.clear(bounds)

        let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
        let radius: CGFloat = 100
        let offset: CGFloat = -90
        let startAngleRadians: CGFloat = (startAngle + offset).radian
        let endAngleRadians: CGFloat = (endAngle + offset).radian

        ctx.beginPath()

        let path = CGMutablePath()
        path.move(to: center)
        path.addArc(
            center: center,
            radius: radius,
            startAngle: startAngleRadians,
            endAngle: endAngleRadians,
            clockwise: false
        )
        ctx.addPath(path)

        ctx.setFillColor(self.tintColor.cgColor)
        ctx.setStrokeColor(UIColor.clear.cgColor)

        ctx.drawPath(using: .fill)
    }

    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(ArcLayer.endAngle) || key == #keyPath(ArcLayer.startAngle) {
            return true
        }

        return super.needsDisplay(forKey: key)
    }
}

カスタムCAShapeLayerを持つViewを作成

final class ArcView: UIView {
    override class var layerClass: AnyClass {
        ArcLayer.self
    }

    private var arcLayer: ArcLayer {
        layer as? ArcLayer ?? .init()
    }

    var endAngle: CGFloat {
        get {
            arcLayer.endAngle
        }

        set {
            arcLayer.endAngle = newValue
            arcLayer.setNeedsDisplay()
        }
    }

    override func tintColorDidChange() {
        super.tintColorDidChange()
        arcLayer.tintColor = tintColor
        arcLayer.setNeedsDisplay()
    }

    func increment(newEndAngle: CGFloat) {
        guard arcLayer.endAngle < 360 else { return }
        arcLayer.removeAllAnimations()

        let anim = CABasicAnimation(keyPath: #keyPath(ArcLayer.endAngle))
        anim.fromValue = endAngle
        anim.toValue = endAngle + newEndAngle
        anim.duration = 0.5

        endAngle = endAngle + newEndAngle
        arcLayer.add(anim, forKey: "increment")
    }

    func decrement(newEndAngle: CGFloat) {
        guard arcLayer.endAngle > 0.0 else { return }
        arcLayer.removeAllAnimations()

        let anim = CABasicAnimation(keyPath: #keyPath(ArcLayer.endAngle))
        anim.fromValue = endAngle
        anim.toValue = endAngle - newEndAngle
        anim.duration = 0.5

        endAngle = max(endAngle - newEndAngle, 0)
        arcLayer.add(anim, forKey: "decrement")
    }
}

Viewの利用

import UIKit

final class CustomCALayerProperty: UIViewController {
    private var arcView = ArcView()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 省略
    }

    private func setupPlusMinusButtons() {
        // ボタンの設定(省略)
        let plusButton = UIButton()
        let minusButton = UIButton()

        plusButton.addAction(.init(handler: { [weak self] _ in
            self?.arcView.increment(newEndAngle: 90)
        }), for: .touchUpInside)

        minusButton.addAction(.init(handler: { [weak self] _ in
            self?.arcView.decrement(newEndAngle: 90)
        }), for: .touchUpInside)

        // 省略
    }
}

以上です。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?