12
12

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 5 years have passed since last update.

iOSでハートのボタンアニメーションをつくる

Posted at

先日「いいね!」的なハートのボタンアニメーションを作る機会がありまして。

先輩がライブラリ公開しているものを参考にしつつ、
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で指定した、unselectedImageselectedImage は、
Storyboard上でカスタマイズできるようになります。

Storyboardで設定

Storyboardでボタンを配置して、

上記のように、Custom Classに作成したImageButtonを設定すると、

このように、unselectedImageselectedImage
好きな画像を設定できるようになります。

つまり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

12
12
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
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?