on
Gesture Recognizer와 Responder Chain
Gesture Recognizer와 Responder Chain
iOS 앱 개발을 하다 보면 CollectionViewCell
을 눌렀는데 반응하지 않거나 하는 상황이 생겨요.
저도 그런 문제를 경험했고, 원인은 Gesture Recognizer
와 Responder Chain
의 관계에 있었어요.
이 글에서는 해당 이슈를 중심으로 iOS의 이벤트 전달 구조를 설명하고, 실서비스에서 어떻게 대응할 수 있는지를 정리해보려고 해요.
문제 상황: 터치가 안 먹히는 이유
실제 서비스에서 자주 발생하는 예시는 다음과 같아요:
CollectionViewCell
/TableViewCell
을 눌러도didSelectItemAt
이 호출되지 않음
이 문제는 단순한 UI 버그처럼 보이지만, 실제로는 상위 뷰에 등록된 Gesture Recognizer가 터치 이벤트를 가로채는 구조적인 원인에서 발생해요.
터치 이벤트 흐름 정리
UIKit은 터치가 발생하면 다음 순서로 이벤트를 전달해요:
UIApplication → UIWindow → rootView → subviews ...
이 과정에서 터치가 도달할 view를 찾기 위해 hitTest(_:with:)
→ point(inside:with:)
가 호출됩니다. 최종적으로 가장 깊은 view가 first responder가 되며, 이벤트를 처리하지 않으면 상위 responder에게 전달됩니다. 이것이 Responder Chain입니다.
Gesture Recognizer는 왜 문제를 일으킬까?
Gesture Recognizer
는 responder chain에 속하지 않지만, UIKit은 터치 이벤트를 responder에게 전달하기 전에 recognizer에게 먼저 전달합니다.
여기서 중요한 포인트
- recognizer가 터치를 성공적으로 인식하면, 해당 터치는 취소(canceled) 됩니다
- 이때 responder는
touchesCancelled
를 받게 되며,touchesEnded
는 호출되지 않습니다 - 기본적으로
UITapGestureRecognizer.cancelsTouchesInView == true
이기 때문에 이 문제가 자주 발생해요
해결 방법 1: cancelsTouchesInView = false
가장 단순한 해결 방법입니다.
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
tap.cancelsTouchesInView = false
backgroundView.addGestureRecognizer(tap)
이렇게 하면 recognizer가 이벤트를 가로채지 않고, 원래 view에게 이벤트가 전달됩니다.
해결 방법 2: UIGestureRecognizerDelegate
활용
보다 정교하게 컨트롤하고 싶다면 delegate 메서드를 활용할 수 있어요.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
// tableview의 subview라면 gesture 비활성화
return !(touch.view?.isDescendant(of: tableView) ?? false)
}
이렇게 하면 특정 뷰에 대해서는 recognizer가 아예 반응하지 않도록 조절할 수 있어요.
마무리하며
이 문제를 처음 마주하면 정말 난감해요. 코드상으로는 아무 문제가 없어 보이고, 디버깅으로도 원인을 파악하기 어렵죠.
그래서 이 구조를 미리 이해하고 있으면, 문제의 본질을 빠르게 파악하고 해결할 수 있어요.
서비스를 만들다 보면 gesture를 사용하는 일이 정말 많은데, 이 흐름을 이해하고 있느냐에 따라 대응 속도가 크게 달라집니다.