[iOS] touchイベントを制御する


## タッチイベントの種類

タッチイベントにはいくつかのイベントがあります。
タッチイベントは「UIResponder」クラスで定義されている以下のメソッドをオーバーライドすることで検知できます。（UIView、UIViewControllerはUIResponderクラスのサブクラスなのでこれらもメソッドを持っています）

| メソッド | 説明 |
|--------|--------|
| `touchesBegan:withEvent:` | タッチ開始時 |
| `touchesMoved:withEvent:` | タッチしたまま指を移動 |
| `touchesEnded:withEvent:` | タッチした指が画面から離れる |
| `touchesCancelled:withEvent:` | タッチがキャンセルされたとき（例えば電話の着信など） |

ちなみに、いくつかのクラスではデフォルトオフになっているため、`setUserInteractionEnabled:`アクセサメソッドに`YES`を渡す必要があります。


## タッチされたviewを調べる

タッチされたviewを調べるには、touchオブジェクトの`view`を参照することで確認できます。

```objc
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];
	NSLog(@"%@", touch.view);
}
```

また、`event#touchesForView:`メソッドに、タッチされたか知りたいviewを指定することで、該当のビューがタッチされたかを確認することができます。

```objc
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	if ([event touchesForView:aView]) {
		//NULLじゃなかったら、`aView`がタッチされている
	}
}
```


## タッチされたlayerを調べる

viewに比べ、layerは若干めんどくさい処理が必要になります。

```objc
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	UITouch *touch = [touches anyObject];

	//タッチされた位置を取得
	CGPoint touchpoint = [touch locationInView:self.view];

	//得られた位置にあるlayerを取得
	CALayer *layer = [self.view.layer hitTest:touchpoint];
}
```



## UIViewのタッチイベントをキャプチャするタイミング

`hitTest:withEvent:`メソッドは画面上どこをタップされても呼び出されます。
HTML的に考えると違和感があるのだけど、メソッド名を見れば自明。
さらにこのメソッドの中で`pointInside:withEvent:`メソッドが呼ばれ、ここで実際に自身がタッチされたかをチェックし、`BOOL`を返すようになっています。

例えばこんな感じ↓

```objc
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent)event
{
	CGRect rect = self.bounds;

	// タッチ位置が自身の`bounds`に含まれるかチェック
	return CGRectContainsPoint(rect, point);
}
```

#### ヒットエリアを拡大する

ちなみにこの判定のメソッドをオーバーライドし、その値を調整することで実際のヒットエリアを変えることができます。（仮にすべてに`YES`を返すと必ずタッチされたことになる）

[ボタンのタップ反応エリアの拡大方法](http://qiita.com/griffin_stewie/items/39c0d555265ee7bb7fc9)を参考にさせて頂いています。

`UIButton`のサブクラスで、以下をオーバーライドし、実装します。

```objc:ExpandHitArea.m
- (BOOL)pointInside:(CGPoint)point
          withEvent:(UIEvent *)event
{
	const CGFloat insetLeft   = 15.0f;
	const CGFloat insetRight  = 15.0f;
	const CGFloat insetTop    = 15.0f;
	const CGFloat insetBottom = 15.0f;

    // ヒット確認用のCGRectを新規に作る
	CGRect rect = self.bounds;

    // 指定したinset分、領域を広げる
	// （位置をオフセットし、幅・高さを足し込む）
	rect.origin.x -= insetLeft;
	rect.origin.y -= insetTop;
	rect.size.width  += (insetLeft + insetRight);
    rect.size.height += (insetTop + insetBottom);

    // 拡張したエリアとの当たり判定を返す
    return CGRectContainsPoint(rect, point);
}
```


### `hitTest:withEvent:`メソッドをオーバーライドしてタッチ対象を判別する

以下のように、`hitTest:withEvent:`メソッド内でSubviewを使って判定を行い、returnするUIViewを変えることでtargetを変えることができます。

```objc
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
	CGRect rect = self.subView.bounds;
	
	// subviewの位置がタッチされていたらsubviewを返す
	if (CGRectContainsPoint(rect, point)) {
		return self.subView;
	}

	return [super hitTest:point withEvent:event];
}
```

returnで返すUViewを固定にすれば、必ずそれがタッチされたことに、なんてこともできます。


---------------


## [2014.07.10 追記] ハマったこと

**親のviewのframeからはみ出した要素はタップに反応しない**

これ、地味にハマりました。
HTML脳だと見えているものは基本、クリックなどに反応してくれるのでしばらく答えにたどり着けずにいました。

要は`aView.clipsToBounds = YES;`にしているとクリッピングされて見えなくなる範囲は、見ていても反応しない、ということです。
自分でイチから作っている場合はすぐに気付けそうですが、人が作ったものに手を入れている場合は予期せぬ構造になっていて「なんで見えているのに反応しないんだー」ってことになるので注意が必要です。
（これに気づくのに数時間要した・・orz）