先日「いいね!」的なハートのボタンアニメーションを作る機会がありまして。
先輩がライブラリ公開しているものを参考にしつつ、
https://qiita.com/darquro/items/c5cdf7dbcad1d5bb0188
自分はライブラリではなく、自作で、
@IBDesignableと@IBInspectableを使って
ハートボタンとそのアニメーションを作ってみましたので、その備忘録。
開発環境
Mac OS Mojave 10.14.6
Xcode 11.0
Swift 5
つくりたいもの
- ハートの画像2枚をボタン押下で切り替える
- ボタン押下時(Highlight時)にハートをちょっと小さくする
- ボタン押下後(Touch up inside)にハートをちょっと大きくする
ハートの画像2枚をボタン押下で切り替える
アニメーションとは付加機能。
まずアニメーションなしで、つくりたいものを実現します。
他にも方法はあると思いますが、今回はハートの画像2枚をボタン押下で切り替えます。
@IBDesignableと@IBInspectableを使った汎用的なボタン
@IBDesignableと@IBInspectableは非常に便利です。
(SwiftUIの世の波とは逆行してる感!)
コードである程度の設定を書いておくと、
Storyboardでカスタマイズが容易になります。
まずImageButtonという汎用的なボタンを設定します。
import UIKit
@IBDesignable
final class ImageButton: UIButton {
@IBInspectable var unselectedImage: UIImage = UIImage()
@IBInspectable var selectedImage: UIImage = UIImage()
public var selectedStatus: Bool = false {
didSet {
setupImageView()
}
}
override func awakeFromNib() {
super.awakeFromNib()
setupImageView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setupImageView()
setNeedsDisplay()
}
private func setupImageView() {
self.setImage(self.selectedStatus ? self.selectedImage : self.unselectedImage, for: .normal)
self.setImage(self.selectedStatus ? self.selectedImage : self.unselectedImage, for: .highlighted)
}
}
@IBInspectable
で指定した、unselectedImage
と selectedImage
は、
Storyboard上でカスタマイズできるようになります。
Storyboardで設定
Storyboardでボタンを配置して、
上記のように、Custom Classに作成したImageButtonを設定すると、
このように、unselectedImage
とselectedImage
に
好きな画像を設定できるようになります。
つまりImageButtonはハートボタン以外にも利用できるので、便利感ありますね。
以下の画像を設定します。
- unselectedImage
- selectedImage
- DefaultのImage <- 忘れないように
アクションを設定
あとはボタンのクリック動作をハンドリングして、処理を書きます。
※Storyboardとコードを結びつけるとこは省略。
@IBOutlet private weak var likeButton: ImageButton!
@IBAction private func clickLikeButton(_ sender: Any) {
likeButton.selectedStatus = !likeButton.selectedStatus
}
画像切り替え 完成
これでアニメーションのない、ハートボタンが完成です。
ハート押下時にアニメーションを
今回は CASpringAnimation
を利用します。
ばねっぽいAnimationを表現しやすいと聞いたので。
今回アニメーションを付けるのは2箇所あります。
- ボタン押下時(Highlight時)にハートをちょっと小さくする
- ボタン押下後(Touch up inside)にハートをちょっと大きくする
ボタン押下時(Highlight時)にハートをちょっと小さくする
方法は色々あると思いますが、自分は以下のコードで実現しました。
isHighlighted
でボタンが押されている状態かを識別します。
override var isHighlighted: Bool {
didSet {
if isHighlighted {
if imageView?.layer.animation(forKey: "reduced-size") == nil {
let animation = CASpringAnimation(keyPath: "transform.scale")
animation.duration = 0.05 // animation時間
animation.fromValue = 1.0 // animation前サイズ
animation.toValue = 0.95 // animation後サイズ
animation.mass = 0.1 // 質量
animation.autoreverses = false // 自動でfromの値に戻らない
animation.initialVelocity = 40.0 // 初速度
animation.damping = 1.0 // 硬さ
animation.stiffness = 40.0 // バネの弾性力
animation.isRemovedOnCompletion = false // animation動作後に完了状態としない
animation.fillMode = .forwards // 一方向モード。fromの形状に戻らない
imageView?.layer.add(animation, forKey: "reduced-size")
}
}
}
}
ポイントは以下。
-
mass
initialVelocity
damping
stiffness
は動作を見てなんとなく値決め - 押下中にサイズが戻ってしまわないように、
autoreverses
isRemovedOnCompletion
はfalse、fillMode
は.forward
- animationは1回だけ実行にしたいので、animationが既に実行されているかどうかの判別式を記載
if imageView?.layer.animation(forKey: "reduced-size") == nil
- animation実行判別のために、任意のkey名
reduced-size
を設定
これでボタン押下時のAnimationは実現できました。
しかし、これでは imageView?.layer.add(animation, …)
で、追加された
animationがremoveされていません。それについては後述。
ボタン押下後(Touch up inside)にハートをちょっと大きくする
こちらも方法は色々あるかと思いますが、今回は上記で設定したsetupImageView()
funcにanimationを記載することで実現しました。
private func setupImageView() {
self.setImage(self.selectedStatus ? self.selectedImage : self.unselectedImage, for: .normal)
self.setImage(self.selectedStatus ? self.selectedImage : self.unselectedImage, for: .highlighted)
let animation = CASpringAnimation(keyPath: "transform.scale")
animation.duration = 0.3 // animation時間
animation.fromValue = 0.95 // animation前サイズ
animation.toValue = 1.0 // animation前サイズ
animation.mass = 0.6 // 質量
animation.initialVelocity = 40.0 // 初速度
animation.damping = 3.0 // 硬さ
animation.stiffness = 40.0 // バネの弾性力
imageView?.layer.add(animation, forKey: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.imageView?.layer.removeAllAnimations() // highlightのAnimationも含め、removeする
}
}
既述したもの以外のポイントは以下。
- animationが全て完了したらremoveする必要があるので、animation時間が経過したらremoveを実施
self.imageView?.layer.removeAllAnimations()
animationのremoveに関してはもっと良い方法があるかも...
指摘などあればぜひコメントお願いしますm(_ _)m
完成
これでアニメーション付きのハートが完成しました。
まとめ
CASpringAnimationについて、
@IBDesignableと@IBInspectableについて、
詳細の説明は省略しました。参考資料をご確認頂ければと。
参考
https://qiita.com/kaway/items/b9e85403a4d78c11f8df
http://www.cl9.info/entry/2018/05/26/175246
https://qiita.com/h-nag/items/7af47ea665332c3ac1bf
https://qiita.com/tasaiii725/items/c70bf648242b061e0734
https://qiita.com/son_s/items/7ca2acf690d10f9fd1b7