나의 터치 이벤트에 반응한 뷰가 누구인지 알아보기 위해 필요한 것이 hitTest!
hitTest의 목적은 터치 이벤트가 발생한 최상단의 뷰를 찾기 위함이다.
point를 포함하고 있는 View에서 가장 멀리 떨어진 View(= 최상단의 뷰)를 반환한다.
여기서 말하는 최상단의 뷰는 View의 hierarcy에서 가장 상위 (ex. UIWindow) 를 말하는 것일까??
NO!!
최상단의 뷰는 사용자가 보기에 가장 위에 있는 뷰를 의미한다. 가장 앞에 있는 View 라고 볼 수 있다.
위와 같이 View A와 View B가 겹쳐저 있는 부분을 터치를 했을 때 가상 상위에 있는 View는 B
이다.
hitTest를 이용하면 사용자의 터치 이벤트를 받는 View를 정할 수 있다.
예를 들어, View B의 hitTest를 이용하여 그림에서와 같은 위치를 터치하더라도 View A를 터치한 것과 같은 효과를 낼 수 있다.
결론 부터 말하자면, View B에서 이벤트를 처리하지 않도록 hitTest에서 nil을 반환 하면 된다.
class ViewB: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitview = super.hitTest(point, with: event)
return hitview == self ? nil : hitview
}
}
nil을 반환하는 경우 해당 이벤트를 처리하지 않고, 이벤트에 반응할 다른 View를 찾으러 간다. View B 다음으로 가장 최상단에 있는 View는 View A 이므로, View A가 이벤트를 처리하게 된다.
hitTest는 가장 앞에 있는 View(= 최상단에 있는 View) 를 찾기 위해서 View 계층을 탐색한다. 이때, 이용하는 방식이 reverse pre-order DFS(Depth-first-traversal) 이다.
예시로 든 화면의 뷰 계층을 그림으로 나타내자면 다음과 같다.
만약 View B의 hitTest
에서 nil
을 반환하게 한다면 어떻게 될까?
다음 최상위 뷰를 찾기 위하여 View A를 탐색하게 되고, 그 결과 터치 이벤트에 반응하는 객체는 View A가 된다.
결과적으로 View B는 hitTest에서 nil을 반환함으로써 View A에게 touch event를 전달 한 것이다.
그럼 이제, Apple에서 제공하는 예시를 이해해 보자.
여러개의 View가 있고, 중첩된 구조 이다.
여기서 만약 View B.1
을 터치하면 event는 어떻게 전달될까?
위에서 부터 아래 계층으로 내려오면서 탐색을 진행한다. View C
까지 탐색을 하지만, View C
는 해당 point
를 포함하지 않기 때문에 false
를 return
하고, View B로 넘어와서 hitTest를 수행한다. 그 결과 View B.1
을 반환하게 된다.
즉, 해당 뷰 계층에서 가장 최상단에 있는 View는 View B.1
이라는 것을 의미한다.
위 경우에 대해서는 hitTest를 무시한다.