iOS/Swift

iOS) Hit Testing과 Responder Chain

seongpil Heo 2026. 3. 5. 02:29
728x90

 

이번에 드롭다운 메뉴 클릭 이벤트를 처리하면서 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
    }
}

 

  1. 터치 좌표가 내 영역(point(inside:with:)인가?
  2. 그렇다면 내 자식 뷰들 중 가장 위에 있는 애부터 hitTest를 호출해라.
  3. 자식 중에 없다면 내가 최종 '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)

 

전달 순서
  1. First Responder : 터치된 View (예: UIButton)
  2. Next Responder : 그 뷰의 Superview (예: UITableViewCell -> UITableView)
  3. Controller : 뷰가 UIViewController의 루트 뷰라면 해당 ViewController로 전달
  4. 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 디버깅이 훨씬 쉬워진다.
728x90