SwiftのCoreAnimationでカードを裏返すアニメーションを実装する

##はじめに。
Swiftを使って簡単なトランプゲームを作ろうとしたところ、トランプカードを裏返す実装が必要になりました。

アニメーションにも色々ありますがUIViewAnimationOptionsでは細かい設定が難しそうなので、CoreAnimationを使うことにしました。開発環境はXcode11,Swift5です。


##実装方法


0.1秒かけてラベルをY軸を中心に90度回した後に、ラベルの文字を変更し、さらに0.1秒かけてラベルを90度回転させます。

```ChatView.swift
import Foundation
import UIKit

class ChatView: UIView {
    
    let myCardImageView = UIImageView()
    let cardAnimateDuration = 0.1
    let zPosition: CGFloat = 10.0
    var myCardNumber: Int!
    
    required override init(frame:CGRect){
        super.init(frame:frame)
        addSubview(myCardImageView)
        myCardImageView.translatesAutoresizingMaskIntoConstraints = false     
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        _ = self.initViewLayout
    }
    
    private lazy var initViewLayout : Void = {
        //layoutSubViewの呼び出しを初回のみにする実装　このあたりは適当
        myCardImageView.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
        myCardImageView.heightAnchor.constraint(equalToConstant: 150.0).isActive = true
        myCardImageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10.0).isActive = true
        myCardImageView.topAnchor.constraint(equalTo: timeResultLabel.bottomAnchor, constant: 20.0).isActive = true
        myCardImageView.image = UIImage(named: "hogehoge")
        myCardImageView.layer.masksToBounds = true
        myCardImageView.layer.cornerRadius = 10.0
        
    }()
       
    public func setNumbers(myNumber: Int) {
            //ここの関数を呼び出してアニメーションを実行させる
            self.myCardNumber = myNumber
            //keyPathは文字列ですが指定できる値が決まっています。
            let animation1 : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.y")
            animation1.duration = cardAnimateDuration
            //初期状態と終了状態を宣言します
            animation1.fromValue = 0
            animation1.toValue = CGFloat(Double.pi/2)
            //以下の2行はアニメーションを終えた後の状態を維持するおまじない
            animation1.isRemovedOnCompletion = false
            animation1.fillMode = CAMediaTimingFillMode.forwards
            animation1.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
            animation1.delegate = self
            animation1.setValue(layer, forKey: "myCardFront")
            myCardImageView.layer.add(animation1, forKey: "myCardFront")

            let animation2 : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.y")
            animation2.duration = cardAnimateDuration
            animation2.fromValue = CGFloat(Double.pi*3/2)
            animation2.toValue = CGFloat(Double.pi*2)
            animation2.isRemovedOnCompletion = false
            animation2.fillMode = CAMediaTimingFillMode.forwards
            animation2.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
            //遅延実行する
            animation2.beginTime = CACurrentMediaTime() + cardAnimateDuration
            animation2.delegate = self
            myCardImageView.layer.add(animation2, forKey: nil)
    
    }

extension ChatView: CAAnimationDelegate {
    
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {   
        if let _ = anim.value(forKey: "myCardFront") as? CALayer  {
            let path = "daiamond" + String(myCardNumber)
            myCardImageView.image = UIImage(named: path)          
        }            
    }        
}


```

Double.pi*/2〜Double.pi*3/2の間を飛ばす実装にすることで、あたかもカードが裏返っているように見えます。逆回りにするにはfromValue,toValueの値を全て負にします。


##詰まったポイント
durationを小さくして回転スピードを速めると、周りの要素が消えたり、回転したいラベルの半分が見えなくなる現象がおきました。zPositionを指定することでこの問題は解決します。

beginTimeを指定して遅延実行を行うことで複数のアニメーションを連続させて動かすことができます。ネストが深くならないのが嬉しいですね。

最初に書いた時はCABasicAnimationインスタンスのkeyPathを勝手に指定したせいでアニメーションが動かずに苦労しました。

delegateメソッドで、一つ目のアニメーション（90度回転）が終了したのを検知してテキストを変えたり、画像を入れ替えたりします。

##実行結果
裏返すと?から5になります。
![画面収録-2020-05-03-18.04.41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240900/3783b07d-87d4-eb26-be84-c71fea4575c0.gif)

##最後に
CoreAnimationは先例があまりなくて調べるのが大変ですね。他に面白そうな実装があれば記事に書いていきたいです。