iOS/UIKit

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

Hamp 2025. 5. 30. 23:25
반응형

👋 들어가기 전

UIKit에서 이벤트를 처리할 때, 항상 HitTest와 touchesBegan을 혼동하고

 

왜 혼동할까라는 근본적인 의문이 있어

이번에 여러가지 실험을 통해 나만의 정리를 한번하려고한다.

 

그전에 이전 부스트 캠프에서 공부했던 내용을 잠깐 정리하고 가보자.

https://hamp.tistory.com/19

 

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

 

반응형