iOS ボタンのタッチ範囲を拡大する方法
pointInside をオーバーライドして感知領域を拡大する
日常の開発でよくあるのは、デザイン通りにUIをレイアウトして画面は綺麗に仕上がっているのに、実際の操作ではボタンの感知範囲が小さくて正確にタップしにくいことです。特に指が太い人には非常に使いづらいです。

完成サンプル図
以前は…
この問題について当初は特に深く調べず、元のボタンの上に範囲を広げた透明な UIButton を無理やり重ねて、その透明なボタンでイベントを受け取る方法を使っていましたが、とても面倒で、コンポーネントが増えると管理もしづらくなります。
後でレイアウトの方法で解決しました。ボタンはレイアウト時に上下左右すべてを0(またはそれ以下)に揃え、imageEdgeInsets、titleEdgeInsets、contentEdgeInsets の3つの内側余白パラメータを調整して、アイコンやボタンのタイトルをUIデザインの正しい位置に配置します。しかし、この方法はStoryboardやxibを使ったプロジェクトに適しており、Interface Builderで直接レイアウトを調整できるためです。もう一つのポイントは、デザインしたアイコンは余白がないものが望ましく、そうでないと位置合わせが難しくなります。時には0.5の距離で調整がうまくいかないこともあります。
その後…
いわゆる「見聞を広げる」ということで、最近新しいプロジェクトに触れてまた一つ小技を学びました。それは UIButton の pointInside メソッドでイベントの反応範囲を広げる方法です。デフォルトでは UIButton の Bounds が使われていますが、その Bounds を拡張してボタンのタップ可能領域を大きくできます!
上記の方法で…私たちは次のことができます:
class MyButton: UIButton {
var touchEdgeInsets:UIEdgeInsets?
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var frame = self.bounds
if let touchEdgeInsets = self.touchEdgeInsets {
frame = frame.inset(by: touchEdgeInsets)
}
return frame.contains(point);
}
}
カスタム UIButton を作成し、touchEdgeInsets という public プロパティを追加して 拡張したい範囲を保持 できるようにします。次に、pointInside メソッドをオーバーライドして、上記のアイデアを実装します。
使用:
import UIKit
class MusicViewController: UIViewController {
@IBOutlet weak var playerButton: MyButton!
override func viewDidLoad() {
super.viewDidLoad()
playerButton.touchEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
}
}

再生ボタン/青色は元のタップ領域/赤色は拡大後のタップ範囲
使用時は、Button の Class を自作の MyButton に指定するだけで、touchEdgeInsets を設定して個別の Button のタップ範囲を拡大できます!
️⚠️⚠️⚠️⚠️️️️⚠️️️️
Storyboard/xib を使用する際は、
Custom Classを MyButton に設定することを忘れないでください
⚠️⚠️⚠️⚠️⚠️
touchEdgeInsetsは(0,0)を中心に外側へ広がるため、上下左右の距離は負の値で拡張します。
いい感じですが…しかし:
すべての UIButton をカスタムの MyButton に置き換えるのは非常に手間で、プログラムの複雑さも増し、大規模プロジェクトでは競合が起こる可能性もあります。
このように、私たちがすべての UIButton に本来備わっているべき機能だと考える場合、可能であれば、元の UIButton を直接 Extension で拡張したいと思います:
private var buttonTouchEdgeInsets: UIEdgeInsets?
extension UIButton {
var touchEdgeInsets:UIEdgeInsets? {
get {
return objc_getAssociatedObject(self, &buttonTouchEdgeInsets) as? UIEdgeInsets
}
set {
objc_setAssociatedObject(self,
&buttonTouchEdgeInsets, newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var frame = self.bounds
if let touchEdgeInsets = self.touchEdgeInsets {
frame = frame.inset(by: touchEdgeInsets)
}
return frame.contains(point);
}
}
使用方法は前述の使用例と同様です。
Extension はプロパティを含めることができず、含めるとコンパイルエラー「Extensions must not contain stored properties」が発生するため、ここでは 使用 Property 配合 Associated Object を参考に、外部変数 buttonTouchEdgeInsets を Extension に関連付けることで、プロパティのように日常的に使用できるようにしています。(詳細な仕組みは 貓大的文章 をご参照ください)
UIImageView (UITapGestureRecognizer) は?
画像のタップについても、自分で View に追加した Tap ジェスチャーと同様に、UIImageView の pointInside をオーバーライドすることで同じ効果を得られます。
完成!継続的な改善により、この問題の解決がさらにシンプルで便利になりました!
参考資料:
付記
去年同じ時期に「小さなことを積み重ねて大きなことに」という小カテゴリを作って、日常の開発での細かいことを記録しようと思いましたが、これらの小さなことがひそかに積み重なって、アプリの体験やプログラム面で大きな成果につながります。結果として1年もかかってようやくもう1記事追加できました<(_ _)>。小さなことは本当に記録し忘れやすいですね!
Post MediumからZMediumToMarkdownを使って変換しました。



コメント