HitTest와 touchesBegan은 무슨 관계가 있을까?

👋 들어가기 전
UIKit에서 이벤트를 처리할 때, 항상 HitTest와 touchesBegan을 혼동하고
왜 혼동할까라는 근본적인 의문이 있어
이번에 여러가지 실험을 통해 나만의 정리를 한번하려고한다.
그전에 이전 부스트 캠프에서 공부했던 내용을 잠깐 정리하고 가보자.
UIResponder Chain
챌린지 때 간단하게만 보고 지나간 iOS에서 이벤트 처리하는 과정을 다시 한번 학습해보자. 처음은 각 과정에서 등장하는 구성요소의 개념부터 학습해보자.1. UIEvent앱에서 하나의 유저 인터렉션
hamp.tistory.com
🏁 학습할 내용
- HitTest의 목적과 흐름
- touchesBegan의 목적과 흐름
- 실험
- 관계 정리
🧨 HitTest
🎯목적
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
HitTest는 현재 event를 누가 가져갈 것인지를 결정한다.
리턴 타입을 보면 UIView? 인 것을보면 가져갈 뷰를 넘겨주면된다.
여기서 리턴 형태는 크게 3가지로 나뉜다.
- super 또는 override를 하지 않음
- nil
- 특정 subview
차이는 흐름에서 알아보자.
🛝 흐름
이전 포스팅을 살펴보면 HitTest는 reversed-pre-order DFS 형태로 찾는다.
하니씩 해석해보면 pre-order은 tree 구조에서 left-root-right 형태인데
reverse가 붙어 있어 right-root-left 형태
view 계층 구간과 연결시키면, 계층이 같은 뷰중 나중에 addSubview된 뷰를 먼저 본다.
이후 DFS를 통해 다음 해당 뷰의 서브뷰 끝까지 탐색, 아무도 받지 않으면
다음 root 노드 뷰 계층을 탐색한다.
정리하면
그러면 위에서 정의된 3가지 형태는 이 흐름에 어떻게 영향을 줄까?
1. Super 또는 override 하지 않음
기본적으로 흐르는 reversed-pre-order DFS를 이용하겠다.
2. nil
reversed-pre-order DFS를 도중에 끊겠다.
내 밑의 Subview들은 HitTest를 안받을꺼니깐 다음으로 넘어가라나와 Subview들은 HitTest를 안받을꺼니깐, 옆으로 가라(나도 포함)
3. 특정 subview
hitTest를 받을 view는 바로 나! 😀
👇 touchesBegan
🎯목적
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
공식문서를 살펴보면 뷰 또는 윈도우에서 터치가 발생하면 호출된다고 한다.
여기서 이번 학습을 한 계기가 나오게된다.
부모-자식 관계로 구성된 뷰 계층에서, 도대체 어디서 호출되는거지??
부모부터 자식까지 이것도 내려오는 건가??
아래 코드와 출력 결과를 통해 한번 알아보자.
🧪 실험
🪜 계층 설명


Container1(주황색)이 Container2(상단), Container3(하단)뷰를 Subview로 갖고있다.
Container2,3은 각 UIView3개, 빨,초,파 Subview3개를 갖고 있는 형태이다.
계층구조를 조금 더 보기 쉽게 만들면 다음과 같다.

이 때 HitTest와 touchesBegan의 상관관계를 살펴보기 위해 HitTest의 3가지 형태를
실험해보자.
모든 Container에 대해 다음과 같은 출력문이 있다.
super 호출 전, super 호출 후, defer
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
defer {
print("Defer","ContainerView1",#function)
print("=========================\n")
}
print("ContainerView1",#function)
let view = super.hitTest(point, with: event)
print("😀HitTest 주인공은 바로 나! Contanier1: \(view == self)")
return view
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
defer {
print("Defer","ContainerView1",#function)
print("=========================\n")
}
print("ContainerView1",#function)
super.touchesBegan(touches, with: event)
}
1️⃣ super 또는 override하지 않을 때
🌀 입력
모든 Container의 hitTest를 기본으로하고 Container1(주황색)을 터치했을 때
📏 결과 예측
우리는 "super 또는 override하지 않을 때" 동작은 기본 reverse pre-order DFS 흐름을 그대로 이용한다.
HitTest의 흐름은 (Window쪽부터는 생략) 다음과 같다.
최상단 view
Container1
[Container3 - View3 - View2 - View 1]
[Contanier2 - View3 - View2 - View1]
여기서 아래까지 모두 내려갔지만, 이벤트를 받지 않았기때문에, 다시 Contanier1까지 올라와
Container1가 결국 받는걸로 처리
✅ 결과

결과는 예측한대로 Container3부터 살펴보고 이후, 아무도 UITouchEvent를 받지 않아.
결국 HitTest를 ContainerView1이 받았다.
여기서 하나 흥미로운점은, hitTest를 받은 Container1의 touchesBegan이 호출된 것!
2️⃣ nil
🌀 입력
이번에는 Container1(가장 상위)의 hitTest를 nil로 바꿔보자.
이러면 Container1의 Sunview들에 대해 hitTest 검사를 하지 않는다.
즉, DFS를 진행하지 않고, 같은 계층의 다른 뷰에게 넘겨준다.
📏 결과 예측
Container2와 Container3의 hitTest는 호출되지 않고
ContainerView1이 HitTset를 그대로 받는다.
이후 Container1의 touchesBegan이 호출될 것
✅ 결과

먼저 첫번 째 가설인, 하위 뷰들에 대한 hitTest는 호출되지 않음 🅾️
하지만 touchesBegan 역시 호출되지 않음 ❌
💡보충 실험
어?? touchesBegan은 왜 호출이 안되지?? nil은 subview만 안 받는거 아닌가??
self로 해보자.

touchesBegan이 호출된다.
hitTest 함수의 역할은 Subview까지는 hitTest가 필요없다가 아닌,
나도 포함한 Subview들은 hitTest가 필요 없다고 의미한다.
3️⃣ 특정 Subview
🌀 입력
Containerview1의 hitTest 결과를 Container3로 지정했다.
📏 결과 예측
Container1에서 hitTest는 Container3로 지정됐으므로, 흐름은 여기서 끊긴다.
이후 Container3 touchesBegan이 호출된다.
✅ 결과

결과가 정확히 일치한다.
😀 소감 및 마무리
여기까지 오니 둘의 관계는 다음과 같이 결론 내릴 수 있다.
hitTest는 이벤트 처리를 담당할 뷰를 탐색하고
탐색 결과 뷰를 기준으로 touchesBegan이 호출된다.
| hitTest | touchesBegan | |
| 목적 | 나와 subview들을 포함한 view들 중 event를 가져갈 뷰를 찾는 로직 |
event처리 맡은 뷰에서 처리를 위해 호출 |
| 탐색 방법 | reverse-pre-order-DFS | 없음 |
| 호출 순서 | 선 | 후 |
출처
https://developer.apple.com/documentation/uikit/uiresponder/touchesbegan(_:with:)
touchesBegan(_:with:) | Apple Developer Documentation
Tells this object that one or more new touches occurred in a view or window.
developer.apple.com