はじめに。
Swiftを使って簡単なトランプゲームを作ろうとしたところ、トランプカードを裏返す実装が必要になりました。
アニメーションにも色々ありますがUIViewAnimationOptionsでは細かい設定が難しそうなので、CoreAnimationを使うことにしました。開発環境はXcode11,Swift5です。
実装方法
0.1秒かけてラベルをY軸を中心に90度回した後に、ラベルの文字を変更し、さらに0.1秒かけてラベルを90度回転させます。
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度回転)が終了したのを検知してテキストを変えたり、画像を入れ替えたりします。
実行結果
最後に
CoreAnimationは先例があまりなくて調べるのが大変ですね。他に面白そうな実装があれば記事に書いていきたいです。