ハイライト処理に touchesBegan(_:with:) を使うのはやめよう!


何事か

あるViewがタップされたときにそれをハイライトさせたいけど、ハイライトの演出をカスタムしたいとき。
私の観測上では touchesbegan 系メソッドを利用してハイライト演出をするというコードが書かれやすいように感じます。ググり方によってはこれを使う方法がトップヒットするのでしょうか。

私はこの手法は良くないと思っているため、その問題点と改善案を紹介します。

よくない例

例えば以下のようなコードです。
ハイライト時に表示される用のViewを用意し、 touchesBegan 系メソッドをオーバーライドしてそのViewの isHidden を書き換えるパターン。

class BadHighlightView: UIView {
    var highlightView: UIView!
    init() { ... } // highlightView を生成して配置

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        highlightView.isHidden = false
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        highlightView.isHidden = true
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        highlightView.isHidden = true
    }
}

なにがいけないのか

touchesEnded 及び touchesCancelled は呼ばれないことがあるため、容易にハイライト状態が維持されて表示がグリッチします。

言い直すと、 touchesBegan が呼び出されたあとに touchesEnded または touchesCancelled が呼ばれる保証がないため ハイライトされっぱなしになる ケースがあります

呼ばれないケースの例

  • 1本の指でタップしながらもう1本の指で他のボタンを操作するなどして画面遷移したとき
  • タップしながらホームボタンを押したとき(OSバージョンによって挙動が違うかも)
  • 複数タップでごちゃごちゃやったとき。条件不明

改善案

経験上、 UIView で作ってる画面構成パーツのどれかを UIButtonUIControl にするだけで解決することが多いように思います。

  • UIButton を配置し setBackgroundImage:forState: で背景色を指定した画像をセットする
  • カスタムView自身の親クラスを UIControl にして isHighlighted をオーバーライドする

UIControl のハイライト状態の管理はUIKitがよしなにやってくれるため上記のようなケースにおいてもハイライト状態が維持されるようなことはありません。

既存構成要素だけではうまくいかない場合は以下のような簡単なハンドル用Viewを差し込むのもありだと思います。

class HighlightHandleView: UIControl {
    var highlightChanged: ((HighlightHandleView, Bool) -> ())?
    override var isHighlighted: Bool {
        didSet {
            highlightChanged?(self, isHighlighted)
        }
    }
}

まとめ

画面要件によって最適な実装の形は変化しますが、原則としてUIKitが標準で提供しているメソッドをそのまま使ったほうが良いです。