Gesture Recognizer와 Responder Chain

Gesture Recognizer와 Responder Chain

iOS 앱 개발을 하다 보면 CollectionViewCell을 눌렀는데 반응하지 않거나 하는 상황이 생겨요.

저도 그런 문제를 경험했고, 원인은 Gesture RecognizerResponder Chain의 관계에 있었어요.

이 글에서는 해당 이슈를 중심으로 iOS의 이벤트 전달 구조를 설명하고, 실서비스에서 어떻게 대응할 수 있는지를 정리해보려고 해요.


문제 상황: 터치가 안 먹히는 이유

실제 서비스에서 자주 발생하는 예시는 다음과 같아요:

이 문제는 단순한 UI 버그처럼 보이지만, 실제로는 상위 뷰에 등록된 Gesture Recognizer가 터치 이벤트를 가로채는 구조적인 원인에서 발생해요.


터치 이벤트 흐름 정리

UIKit은 터치가 발생하면 다음 순서로 이벤트를 전달해요:

UIApplication → UIWindow → rootView → subviews ...

이 과정에서 터치가 도달할 view를 찾기 위해 hitTest(_:with:)point(inside:with:)가 호출됩니다. 최종적으로 가장 깊은 view가 first responder가 되며, 이벤트를 처리하지 않으면 상위 responder에게 전달됩니다. 이것이 Responder Chain입니다.

ResponderChain


Gesture Recognizer는 왜 문제를 일으킬까?

Gesture Recognizer는 responder chain에 속하지 않지만, UIKit은 터치 이벤트를 responder에게 전달하기 전에 recognizer에게 먼저 전달합니다.

여기서 중요한 포인트


해결 방법 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를 사용하는 일이 정말 많은데, 이 흐름을 이해하고 있느냐에 따라 대응 속도가 크게 달라집니다.