🕌

Swift / UIKit で良い感じのボタンアニメーション

2020/10/06に公開

毎度〜!!
UI/UXデザイナーのうっくんです!
最近はSwiftでUIを実装したりもやっています。

今回はiOSのUIKitを使ったアニメーションの実装についてご紹介。

まずは完成図

UIKitの標準のアニメーションで用意されている、「スプリングエフェクト(バネ効果)」的なものを使って、良い感じに「ポヨーン」とさせています。

パラメーターをちょこちょこいじってまあまあ気持ちいい感じのやつができたので、メモがわりに公開しておきます。

ついでにGitHubにプロジェクトごと公開しておきました。
コード書くのがめんどくさい人は、ここから直接Xcodeファイルを開けばビルドしてシミュレーターで確認できます。
https://github.com/wecken/UIButtonSpringEffect

コード

Xcode12でやりました。
初心者の方は「iOS App」のテンプレを使って、ファイルの中にある「ViewController.swift」の中に下記のコードをコピペすれば動きます。
中・上級者の方は、適当においしいところだけコピペして使ってください。

//
//  ViewController.swift
//  ButtonSpringAnimation

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        button.layer.cornerRadius = 60 / 2
        button.layer.cornerCurve = .continuous
        button.backgroundColor = UIColor.blue
        button.setTitle("押してみ", for: .normal)
        
        let constraints = [
            button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -80),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 60)
        ]
        NSLayoutConstraint.activate(constraints)
        button.addTarget(self, action: #selector(self.buttonTapped(sender:)), for: .touchUpInside)
    }
    
    @objc func buttonTapped(sender:UIButton) {
        animateView(sender)
    }
    
    func animateView(_ viewToAnimate:UIView) {
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: {
            viewToAnimate.transform = CGAffineTransform(scaleX: 1.08, y: 1.08)
        }) { (_) in
            UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: .curveEaseOut, animations: {
                viewToAnimate.transform = .identity
                
            }, completion: nil)
        }
    }


}


解説

この先はメモなので読まなくても別に困りません。


    func animateView(_ viewToAnimate:UIView) {
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: {
            viewToAnimate.transform = CGAffineTransform(scaleX: 1.08, y: 1.08)
        }) { (_) in
            UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: .curveEaseOut, animations: {
                viewToAnimate.transform = .identity
                
            }, completion: nil)
        }

アニメーションをやっているのはこの部分。

UIKitで一番簡単なアニメーション用のAPIである、UIView.animateを使っています。
このAPIを使うメリットは、実装が簡単なこと。

デメリットは、動かすパラメーターによってはアニメーションできないと言うことがあります。
例えばUILabelのメソッドである、「attributedText」のようなものをアニメーションさせることができません。
例:Labelの色を徐々に変える。フォントサイズを徐々に変える。ベースラインをずらして、移動するようなアニメーションをつくる。などができません。その場合はもう少し実装が複雑になるのですが、Core Animation関連のAPIを使って実装する感じになります。
デメリットもう一つ。複雑なアニメーションを頑張って実装するとコードがどんどんネストしていって読みづらくなるのと、パフォーマンス的にも微妙っぽい。

UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: {
...
} ...

この部分はアニメーションの前半に起こる内容を書いています。
withDuration(実行時間)が0.2で、delay(遅延)が0、optionにcurveEaseIn(アニメーションお前半をゆっくり、後半を速く変化させる。)を設定してます

animations {}のなかには変更するパラメーターを記述。

viewToAnimate.transform = CGAffineTransform(scaleX: 1.08, y: 1.08)

今回は、functionの引数として受け取ったUIView(=viewToAnimate)のスケールを縦横1.08倍にしています。CGAffineTransformと言うCoreGraphicのAPIが出てきますが、「アフィン変換」と言うグラッフィックの処理をやってくれるAPIのようです。アフィン変換の意味は僕には分かりませんが、図形をいい感じに変形させてくれると言うことです。

アニメーションをスローで実行すると、ボタン押下直後、ボタンの大きさがちょっと大きくなっているのが分かります。

それがこのコードの部分です。

その後実行されるコードがこちら。

UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: .curveEaseOut, animations: {
	viewToAnimate.transform = .identity

}, completion: nil)

同じように、UIView.animateを呼んでいますが、今回は渡しているパラメータが微妙に違います。
これがスプリング(バネ効果)を実現してくれている部分です。
withDurationとdelayの意味は先ほどと同じですが、usingSpringWithDampingと、initialSpringVelocityを加えることで、「ポヨーーーン」が可能になります。
usingSpringWithDampingはバネの抵抗みたいな感じで、1が最大、0が抵抗なしです。なので、1にすると抵抗がデカすぎて全然ポヨンポヨンしません。逆に0だと抵抗がなさすぎて、めっちゃバインバインします。いろいろ調整してこの数値に落ち着きました。

initialSpringVelocityはバネを弾く際の初速(強さ)です。
documentationによると

The initial spring velocity. For smooth start to the animation, match this value to the view’s velocity as it was prior to attachment.
A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.

クソむずいので、気になる方は自分で訳しましょう。

で、アニメーションとして変更している値がこちら。

	viewToAnimate.transform = .identity

先ほどaffineTransformを使って大きくしたviewを.identity(元の状態に戻す)と言うのをやっています。

以上です

今後もUIのフロントとかスタイル寄りの実装に関する内容を投稿していく予定です。
よろしくお願いします!

Discussion