
이번에 드롭다운 메뉴 클릭 이벤트를 처리하면서 Hit Testing과 Responder Chain에 대해 알게 되어
간략하게 정리하는 글을 작성해보려고 한다.
위의 클릭 이벤트 관련 및 트러블 슈팅 글은 아래 링크를 참고 바란다.
마이페이지 내가 게시한 글 화면 구현 #4 - Trouble Shooting (Hit Testing, Responder Chain)
👨🏻💻 오늘의 작업[ 1. 드롭다운 뷰가 켜져있을 때, 테이블 뷰 스크롤 막기 ] // 삭제하기 드롭다운 개폐reactor.state.map { $0.isMenuOpen } .observe(on: MainScheduler.asyncInstance) .subscribe(onNext: { [weak self] is
coding-pill.tistory.com
🎯 Hit Testing (어떤 뷰를 눌렀나?)
Hit Testing이란 사용자가 화면을 터치했을 때, 그 좌표가 어느 뷰의 영역에 속하는지 찾아내는 과정이다.
방식
Reverse Pre-order Tree Traversal (역프리오더 트리 순회) 방식을 사용한다.
즉, 가장 나중에 추가된(가장 위에 있는) 자식 뷰부터 검사한다.
※ Reverse Pre-order Tree Traversal
역프리오더 순회는 일반적인 전위 순회(Root-Left-Right)와 달리
루트(Root) → 오른쪽 서브트리 → 왼쪽 서브트리 순서로 노드를 방문하는 방식이다.
※ 역프리오더 트리 순회 예시

다음과 같이 트리가 있을 때 역 프리오더 트리 순회의 순서는 아래와 같다.
A, C, F, B, E, D
메서드
모든 UIView는 hitTest(_:with:)와 point(inside:with:) 메서드를 가진다.
class ExpandableButton: UIButton {
// 터치 영역을 상하좌우로 확장하고 싶은 크기
var touchInset: CGFloat = 20.0
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1. 유저 상호작용, 숨김 여부, 알파값 체크 (기본 조건)
guard isUserInteractionEnabled, !isHidden, alpha > 0.01 else { return nil }
// 2. 현재 내 bound를 touchInset만큼 확장한 영역을 생성
let expandedFrame = bounds.insetBy(dx: -touchInset, dy: -touchInset)
// 3. 터치된 지점이 확장된 영역 안이라면 내가 터치를 먹는다!
return expandedFrame.contains(point) ? self : nil
}
}
- 터치 좌표가 내 영역(point(inside:with:)인가?
- 그렇다면 내 자식 뷰들 중 가장 위에 있는 애부터 hitTest를 호출해라.
- 자식 중에 없다면 내가 최종 'Hit-Test View'가 된다.
※ 핵심 포인트
userInteractionEnabled = false, isHidden = true, alpha < 0.01, 자식 뷰가 부모 뷰의 bounds를 벗어났을 때는
Hit Test 대상에서 제외된다.
참고
hitTest(_:with:) | Apple Developer Documentation
Returns the farthest descendant in the view hierarchy of the current view, including itself, that contains the specified point.
developer.apple.com
🔗 Responder Chain (이벤트를 누가 처리하나?)
Hit Testing을 통해 "이 터치는 이 뷰가 주인이다!"라고 결정되면, 그 뷰가 First Responder가 된다.
하지만 이 뷰가 이벤트를 처리하지 못하거나 무시하면, 이벤트는 Responder Chain을 타고 상위 객체로 전달된다.
구성 요소
UIResponder를 상속받는 모든 객체 (UIView, UIViewController, UIwindow, UIApplication)
전달 순서
- First Responder : 터치된 View (예: UIButton)
- Next Responder : 그 뷰의 Superview (예: UITableViewCell -> UITableView)
- Controller : 뷰가 UIViewController의 루트 뷰라면 해당 ViewController로 전달
- Window & App : 최종적으로 UIWindow, UIApplication, AppDelegate 순으로 전달
전달 경로 : View → SuperView → ViewController → Window → Application
핵심 개념
모든 UI 구성 요소가 UIResponder를 상속받기 때문에 가능한 메커니즘이다.
참고
UIResponder | Apple Developer Documentation
An abstract interface for responding to and handling events.
developer.apple.com
📝 Hit Testing, Responder Chain 요약 및 비교
| 구분 | Hit Testing | Responder Chain |
| 목적 | 터치 지점의 주인(View) 찾기 | 찾은 주인이 처리 안 할 때 대신할 사람 찾기 |
| 방향 | 상위 뷰 -> 하위(자식) 뷰 (Downwards) | 하위 뷰 -> 상위(부모) 뷰 (Upwards) |
| 결과 | First Responder 결정 | 이벤트 핸들링 (Action 실행) |
- Hit Testing은 누구냐(Who)를 찾는 과정
- Responder Chain은 어떻게(How) 처리할지 결정하는 과정
- 이 두 원리를 이해하면 복잡한 UI 디버깅이 훨씬 쉬워진다.
'iOS > Swift' 카테고리의 다른 글
| Swift - WKWebView란? (0) | 2026.03.21 |
|---|---|
| 애플 개발자 계정 등록하기 (0) | 2026.02.11 |
| CoreLocation이란 뭘까? (0) | 2025.10.17 |
| KeyChain에 저장하기 (0) | 2025.09.09 |
| 카카오로 간편 로그인하기 (1) | 2025.09.08 |