プログラミング

【Swift】CAGradientLayerのグラデーションをアニメーションでループさせる

swift-eyecatch

iOSアプリでグラデーションを表現するときは、CAGradientLayerを使用することになると思いますが、そのグラデーションカラーをアニメーションで変化をつける方法のサンプルを記載します。

大きく分けて、CAGradientLayerの実装とアニメーションの実装、アニメーションのループの3パートにわけて記載します。

グラデーションを実装する

swiftでグラデーションを表現するには、CAGraidentLayerインスタンスを作成し、UIViewなどにinsertSublayerします。

fileprivate let gradientStartColor: UIColor = .init(hex: "007AB7")
fileprivate let gradientEndColor: UIColor = .init(hex: "C54F91")

fileprivate lazy var gradientLayer: CAGradientLayer = {
    let gradient = CAGradientLayer()
    gradient.frame = view.bounds
    gradient.colors = [gradientStartColor, gradientEndColor]
    gradient.startPoint = CGPoint(x: 0, y: 0)
    gradient.endPoint = CGPoint(x: 1, y: 1)
    gradient.drawsAsynchronously = true
    return gradient
}()
view.layer.insertSublayer(gradientLayer, at: 0)

一例ですが、このようにCAGradientLayerを作成し、UIViewControllerviewinsertSublayerします。

これでグラデーションを表現することが出来ます。

グラデーションのアニメーションを実装する

swiftでアニメーションを実装する一番簡単な方法は、UIView.animateかと個人的には思いますが、こちらは出来ることが限られています。より詳細にアニメーションを実装したい場合は、CABasicAnimationを使う必要があります。

CAGradientLayerの何かしらのプロパティをアニメーションで変化させたいときはには、このCABasicAnimationが必要になります。

fileprivate let animationIdentifier: String = "colorChange"
fileprivate let animationTarget: String = "colors"

let anim = CABasicAnimation(keyPath: animationTarget)
anim.fromValue = [gradientStartColor.cgColor, gradientEndColor.cgColor]
anim.toValue = [gradientEndColor.cgColor, gradientStartColor.cgColor]
anim.duration = 2.0
anim.fillMode = .forwards
anim.isRemovedOnCompletion = false
gradientLayer.add(anim, forKey: animationIdentifier)

CABasicAnimationインスタンスをイニシャライズする際に、何をアニメーションするのかを指定します。今回は、グラデーションの色にアニメーションをつけたいので、"colors"と指定します。

次に、変化元と変化後の値を、fromValuetoValueに指定します。

isRemovedOnCompletionは、アニメーション完了後に、アニメーション前の状態に戻すか、そのまま維持するか、を指定することが出来ます。

この状態で実行すると、1度だけアニメーションが実行されるようになります。

グラデーションのアニメーションを永続的に繰り返す

アニメーションを永続的に繰り返す最も簡単な方法は、repeatCount.inifinityを指定することです。シンプルなアニメーションを実装するのであれば、これで事足りますが、今回はグラデーションカラーのアニメーションをスムーズに行いたかったので、別の方法を記載します。

// CABasicAnimationのインスタンスにdelegateを設定
anim.delegate = self

// delegateでアニメーション完了時の処理を追記
extension ViewController: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        print("Animation did stopp")
    }
}

CABasicAnimationのインスタンスの delegateを活用して、animationの開始時・終了時に処理を記述することが出来ます。今回は、アニメーション終了時にアニメーションを再度実行する、という処理を追加することにより、アニメーションを永続的に実行するようにします。

その際に、何でも良いのですが、フラグを用いて、animationのfromValuetoValueを都度入れ替え、グラデーションカラーの変化がスムーズになるように実装しています。

以下、サンプルの全コードです。

import UIKit

class ViewController: UIViewController {

    fileprivate var animateFlag: Bool = true
    fileprivate let animationIdentifier: String = "colorChange"
    fileprivate let animationTarget: String = "colors"
    fileprivate let gradientStartColor: UIColor = .init(hex: "007AB7")
    fileprivate let gradientEndColor: UIColor = .init(hex: "C54F91")

    fileprivate lazy var gradientLayer: CAGradientLayer = {
        let gradient = CAGradientLayer()
        gradient.frame = view.bounds
        gradient.colors = [gradientStartColor, gradientEndColor]
        gradient.startPoint = CGPoint(x: 0, y: 0)
        gradient.endPoint = CGPoint(x: 1, y: 1)
        gradient.drawsAsynchronously = true
        return gradient
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.layer.insertSublayer(gradientLayer, at: 0)
        animateGradient()
    }

    fileprivate func animateGradient() {
        let anim = CABasicAnimation(keyPath: animationTarget)
        if animateFlag {
            anim.fromValue = [gradientStartColor.cgColor, gradientEndColor.cgColor]
            anim.toValue = [gradientEndColor.cgColor, gradientStartColor.cgColor]
        } else {
            anim.fromValue = [gradientEndColor.cgColor, gradientStartColor.cgColor]
            anim.toValue = [gradientStartColor.cgColor, gradientEndColor.cgColor]
        }
        anim.duration = 2.0
        anim.fillMode = .forwards
        anim.isRemovedOnCompletion = false
        anim.delegate = self
        gradientLayer.add(anim, forKey: animationIdentifier)
    }
}

extension ViewController: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        animateFlag = !animateFlag
        animateGradient()
    }
}

extension UIColor {
    convenience init(hex: String, alpha: CGFloat = 1.0) {
        let v = Int("000000" + hex, radix: 16) ?? 0
        let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255
        let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255
        let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255
        self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1))
    }
}

CAGradientLayerのアニメーションを実装する機会はこれまでなかったのですが、今回やってみて、意外と簡単に出来るものだなと思いました。

以上です。