내 아이폰에 터치를 해보았다(Reverse-preorder DFS)

2025. 8. 26. 17:40·iOS

iPhone 화면을 터치했을 때 정확히 어떤 일이 일어날까요??

오늘은 iOS에서 터치 이벤트가 어떻게 흘러가는지, 왜 그런 방식으로 설계되었는지에 대해 자세히 알아보겠습니다! 💻

터치 이벤트의 주요 구성 요소

UITouch란?

UITouch는 화면에서 발생하는 터치의 위치, 크기, 움직임, 압력 등을 나타내는 객체입니다.

터치가 시작되면 UITouch 인스턴스가 생성되고, 손가락이 화면에서 떨어질 때까지 계속 유지됩니다.

이게 중요한 이유는 iOS가 터치를 "연속된 하나의 이벤트 스트림"으로 처리하기 때문이에요.

UITouch는 다음과 같은 5가지 상태를 가져요:

  • .began: 손가락이 처음 화면에 닿은 순간 (딱 한 번만 발생)
  • .moved: 손가락이 화면에서 움직일 때
  • .stationary: 손가락이 화면에 있지만 움직이지 않을 때
  • .ended: 손가락이 화면에서 떨어질 때 (딱 한 번만 발생)
  • .cancelled: 시스템이 터치 스트림을 중단할 때 (예: 홈 버튼 터치)

UIEvent란?

UIEvent는 사용자와 앱 간의 단일 상호작용을 설명하는 객체입니다.

하나의 UIEvent는 여러 개의 UITouch를 포함할 수 있어요.

예를 들어 멀티터치 제스처에서 두 손가락으로 핀치하면, 하나의 UIEvent 안에 두 개의 UITouch가 들어있는 거죠.

결국 UITouch와 UIEvent를 먼저 설명한 이유는 터치고 이벤트고 모두 객체라는 것을 강조하기 위해서입니다!

Main Event Loop ➰

그럼 터치 이벤트는 어디서 시작될까요? 바로 Main Event Loop에서 시작됩니다!

앱이 실행되면 UIKit이 메인 런루프를 설정하고, 시스템 이벤트를 감지하기 시작해요. 터치가 발생하면 다음과 같은 과정을 거칩니다:

  1. 하드웨어 레벨에서 터치 감지
  2. 시스템이 터치 데이터를 UIKit으로 전달
  3. UIKit이 UIEvent 인스턴스를 생성
  4. UIApplication.shared.sendEvent(_:)를 통해 메인 이벤트 루프로 전달

이 과정이 메인 스레드에서 동기적으로 처리됩니다. 왜 메인 스레드일까요?

UI 업데이트는 반드시 메인 스레드에서 해야 하고, 터치 이벤트 처리는 UI와 직결되어 있기 때문이에요.

Hit Testing: 누가 터치를 받을까? ⚡️⚡️⚡️

자, 이제 터치 이벤트가 생성됐으니 누가 이 터치를 받을지 결정해야겠죠? 이걸 Hit Testing이라고 합니다.

Hit Testing의 원리

가장 하위 뷰 = 가장 바깥 쪽에 있는뷰 = ViewC

가장 상위 뷰 = 가장 안쪽에 있는 뷰 = 메인뷰입니다

이를 위해 두 가지 핵심 메서드를 알아야 합니다:

1. point(inside:with:) 메서드

주어진 점이 뷰의 경계(bounds) 안에 있는지 확인하는 메서드입니다.

  • point: CGPoint: 뷰의 로컬 좌표계(Frame이 아닌 bounds 기준)
    • 예: 뷰의 bounds가 (0,0,100,100)이면, 유효한 point는 (0,0)부터 (100,100) 사이
  • event: UIEvent?: 이 메서드 호출을 야기한 이벤트 객체

왜 Frame이 아닌 Bounds를 사용할까요? 🤔
frame은 부모 뷰의 좌표계 기준으로 뷰의 위치와 크기를 나타냅니다. 따라서 frame 좌표로 터치를 검사하면, 뷰가 여러 단계 중첩되어 있을 경우 좌표 변환을 복잡하게 여러 번 해야 합니다.
반면, bounds는 자기 자신(view)의 고유 좌표계이므로, point(inside:with:)는 항상 자신 내부에서만 작동합니다. 따라서 좌표 변환 없이 일관되게 범위를 판단할 수 있어요.

2. hitTest(_:with:) 메서드

지정된 점을 포함하는 View계층에서 가장 멀리있는 자손을 리턴합니다.

음... 역시 공식문서 보고는 뭔지 모르겠네요. 😅 간단하게 말하자면:

주어진 점을 포함하는 가장 앞에 있는 뷰(최하위뷰)를 찾아 반환합니다

Hit Test 과정

자 예시의 뷰 계층 구조입니다.

UIWindow (0,0,400,400)
    └── MainView (0,0,400,400)
        ├── ViewA (50,50,100,100)
        ├── ViewB (100,100,150,150) 
        └── ViewC (200,200,100,100)

그러면 어떻게 최하위 뷰를 찾아갈까요?

Reverse-preorder DFS입니다

일반적인 Preorder가 Root->Left->Right였잖아요? Reverse는 Root->Right->Left라고 합니다

ViewA와 ViewB가 겹쳐져 있는 (120,120) 지점을 터치했다고 가정해봅시다:

  1. UIWindow.hitTest(_:with:) 호출
  2. UIWindow의 point(inside:with:) 확인 → true → 하위뷰로 진행
  3. MainView의 hitTest(_:with:) 호출
  4. MainView의 point(inside:with:) 확인 → true → Reverse Preorder에 의해 ViewC부터 확인
  5. ViewC의 hitTest(_:with:) 호출
    1. 좌표변환: (120,120) → (-80,-80)
  6. ViewC의 point(inside:with:) 확인 → false
  7. ViewB의 hitTest(_:with:) 호출
    1. 좌표변환: (120,120) → (20,20)
  8. ViewB의 point(inside:with:) 확인 → true → 하위뷰가 없음 → ViewB 선택! (너 당첨)

핵심 포인트

Hit Testing의 핵심 흐름은 다음과 같습니다:

  1. hitTest 호출
  2. point(inside:with:) 체크
  3. 하위뷰 재귀 검사

더 복잡한 계층 구조에서는 ViewB에 하위뷰가 있다면, 같은 방식으로 Reverse preorder DFS를 통해 가장 앞에 있는 뷰를 찾아갑니다.

이제 이 그림을 다시 보면 흐름이 이해가 갈 겁니다.

ViewB까지는 동일하지만 ViewB에는 또 다른 하위 뷰가 있기 때문에 Reverse preorder DFS로 ViewB.2를 보고

어? 이거 안되네? 같은 계층인 ViewB.1을 보고 어~ 너 되는구나? 근데 너 자식이 없어? 너 당첨~

주의사항: hitTest가 nil을 리턴하는 경우

다음 경우들에서는 hitTest가 nil을 반환합니다:

  1. isUserInteractionEnabled = false (뷰가 사용자 상호작용 비활성화)
  2. alpha ≤ 0.01 또는 isHidden = true (뷰가 보이지 않음)
  3. 지정된 터치 지점이 뷰 계층 구조 외부에 있는 경우

더 명확하게 복습하기 위해 다이어그램을 구해왔습니다:

hitTest 다이어그램

한번씩 다시 보면서 위에서 설명했던 로직들을 한번더 복습하고 가봅시다


Responder Chain

자 그러면 우리가 찾는 저 최하위뷰가 First Responder가 됩니다. 어? 어디서 자주 듣지않았나요?

https://codeisfuture.tistory.com/141

 

SceneDelegate가 UIResponder를 상속하는 이유

왜 갑자기 UIScene이 등장했을까?iOS 13이 출시되면서 가장 큰 변화 중 하나가 바로 UIScene의 도입이었습니다.왜 Apple이 기존에 잘 작동하던 AppDelegate 구조를 바꿔가며 이런 복잡한 시스템을 도입했을

codeisfuture.tistory.com

 

맞아요 바로 이전 글에서 UIScene을 이야기하면서 잠깐 이야기를 했었죠. 복습하고자 다시 정리 해볼게요

First Responder = 이벤트를 가장 먼저 받을 수 있는 응답자

Hit Testing을 통해 선택된 최하위뷰가 바로 이 First Responder가 되는 거죠.

Responder Chain 흐름을 위의 예시를 다시 사용해볼게요:)

ViewB (First Responder)(처리 못하면 전달) -> MainView  -> UIViewController -> UIWindow -> UIWindowScene -> UIApplication -> UIApplicationDelegate

이렇게 흐름에 따라서 ViewB가 UIEvent를 처리하지 않는다면 상위로 이동하면서 최종적으로 UIApplicationDelegate까지 가고처리하지 않는다면 그제서야 이벤트가 버려집니다.

후기?

확실히 저번 UIResponder를 공부한지 하루만에 이어서 터치이벤트까지 학습하니 머리속에 더 정리가 잘되는 것 같습니다.

혹시나 틀린 내용이 있다면 알려주시면 더 공부해보겠습니다 감사합니다:)

참고 문서

https://lena-chamna.netlify.app/post/hit_testing_in_ios/

 

View 계층 탐색 알고리즘과 Hit-Testing in iOS


explain how to find UIView object to handle events with reverse pre-order depth-first traverse algorithm and Hit-testing

lena-chamna.netlify.app

https://ios-development.tistory.com/327

 

[iOS - swift] HitTest 개념 (first responder, responder chain)

Hit Testing이란? 터치 이벤트가 발생한 최상단 뷰를 찾는 행위 -> 찾는 이유: First Responsder (해당 이벤트를 처리할 수 있는 첫 번째 뷰를 탐색하는 것) Hit Testing 동작 원리 최상단 뷰 탐색(역순 탐색):

ios-development.tistory.com

 

'iOS' 카테고리의 다른 글

PHPickerController의 UTI 활용법  (4) 2025.08.29
포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter  (5) 2025.08.27
SceneDelegate가 UIResponder를 상속하는 이유  (2) 2025.08.25
근본으로돌아가자(7)-String,Array으로 시작해서 Sequence까지  (0) 2025.04.04
Metal(2)-셰이더 코드 작성까지  (0) 2025.04.02
'iOS' 카테고리의 다른 글
  • PHPickerController의 UTI 활용법
  • 포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter
  • SceneDelegate가 UIResponder를 상속하는 이유
  • 근본으로돌아가자(7)-String,Array으로 시작해서 Sequence까지
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (133)
      • SWIFT개발일지 (28)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (42)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (2)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
내 아이폰에 터치를 해보았다(Reverse-preorder DFS)
상단으로

티스토리툴바