カスタムボタンを作成してボタン同時押しを防ぐ!(ついでにフィードバックアニメーションをつける)

エンジニアの德光です。 あまり労力をかけず、それでいて(そこそこ)堅牢なアプリを開発するための小技を書いていき…

エンジニアの德光です。
あまり労力をかけず、それでいて(そこそこ)堅牢なアプリを開発するための小技を書いていきたいと思います。

◆UIButtonの同時押し回避

たとえば、複数のボタンそれぞれにSequeによる画面遷移を設定した場合。
両方のボタンをタップした状態で同時に指を離すと、両方の画面遷移が実行されてしまいます。
それって望んだ動作ですか?

これを回避するには、UIButtonの「isExclusiveTouch」を「true」にすればオッケーです。
ボタンのタップが排他的に扱われるようになるため、同時押し状態になることがありません。
画面内のボタンそれぞれに設定していっても良いのですが、悲しいことにInterfaceBuilderに項目が用意されていないのです。(そのうちしれっと増えたりするのでしょうか?)
そのため、各画面でOutletを結んで設定する必要があり、とても面倒です。
なにより、ボタンが増減したときに設定漏れがあったりするといやなので、さくっと下記のようにカスタムボタンクラスを定義してしまい、Storyboard上などで使うボタンをUIButtonからExButtonに変更することで対応を終わらせちゃいましょう。
同時押しが必要になる場合がでてきたら、その時はその時です!
(通常のUI設計では、同時押しを必要とすることは、滅多にないと思います)

// ボタンの共通クラス
public class ExButton: UIButton{
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }
    //Storyboard上で生成したもので呼ばれる
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }
    func commonInit() {
        self.isExclusiveTouch = true    //排他タッチ
        self.backgroundColor = UIColor.lightGray
        self.layer.cornerRadius = 10.0
    }
}

ついでなので、背景色をつけて角丸にして、ただおいただけのボタンでも識別しやすくしています。
テスト用のソース記述する場合には、このくらい変化があれば充分なので、とりあえずこれで。
……と、ちょっと見た目に凝ったら欲がでたので、ちょこっと機能追加を。

◆ボタンタップ時のフィードバック

ボタンをタップしたときに、なんらかのフィードバックがあるとわかりやすくて良いですね。
標準のままでも、ボタンの文字色が変更されるので良いのですが、小さいボタンだとわかりにくいし、もう少し派手なエフェクトが欲しいなとか。
というわけで、タップ時にボタンを拡大させてみたいと思います。

▼実現方法:

1) ボタンを押下したときのイベントを取得する
2) 拡大アニメーションをする
以上の2点を実装すれば、良さそうな気がします。

1) ボタンを押下したときのイベントを取得する

    override public func touchesBegan(_ touches: Set, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        self.animateButton() //拡大アニメーション処理
    }

2) 拡大アニメーションをする

拡大アニメーションといっても、ふくらんで戻って欲しいので、.autoreverseを指定します。

    func animateButton() {
        self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
        UIView.animate(withDuration: 0.15, delay: 0.0, options: [.autoreverse], animations: {
            self.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.2)
        }) { (isCompletion) in
            self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
        }
    }

▼結果:

試すとわかるのですが、なんか想定したアニメじゃないのです。拡大・縮小したあとにボボボッとなるのです。
というわけで、.autoreverseは使わずに、多段アニメーションで対応します。

    func animateButton() {
        self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
        UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: {
            self.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.2)
        }) { (isCompletion) in
            UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: {
                self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
            }) { (isCompletion) in
                self.isAnimating = false
            }
        }
    }

▼機能拡張:

これでアニメは良い感じになりました。
ただ、これだとボタンをタップした場合には良い感じですが、長押しした場合にちょっと寂しいのです。ボタンを離した時にもフィードバックが欲しくなってきます。
そこで、以下を追加して、ボタンの長押しでの動作確認をすると良い感じ!?

    override public func touchesEnded(_ touches: Set, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        self.animateButton()
    }

▼問題発生!?:

長押しした場合のアニメーションは、押した時と離した時に実施され、見た目的にも良い感じではあります。
でも、普通にタップした時に、押した時のアニメーション中に、離した時のアニメーションも実施しようとして、なんだか汚い見た目になってしまいました。
押している間は膨らんだままにするという解決方法もありますが、今回はタップした場合には1回のアニメーションに留めておき、長押し状態だと2回アニメーションするような動作にしたいと思います。
アニメーション中かどうかをフラグで管理して、アニメーション中なら却下しちゃえば解決ですね。
というわけで、最終的に下記のような実装になりました。
拡大率やDurationなどひ定数を使うべきとか、ツッコミどころは多々ありますが、とりあえず今回はこれで完了っと。

import Foundation
import UIKit

// ボタンの共通クラス
public class ExButton: UIButton {
    var isAnimating: Bool = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }
    //Storyboard上で生成したもので呼ばれる
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }
    func commonInit() {
        self.isExclusiveTouch = true    //排他タッチ
        self.backgroundColor = UIColor.lightGray
        self.layer.cornerRadius = 10.0
    }
    override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        self.animateButton()
    }
    override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        self.animateButton()
    }
    func animateButton() {
        if self.isAnimating == true {
            return  //アニメーション中は別のアニメーションは却下
        }
        self.isAnimating = true
        self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
        UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: {
            self.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.2)
        }) { (isCompletion) in
            UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: {
                self.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
            }) { (isCompletion) in
                self.isAnimating = false
            }
        }
    }
}