'hit-test'에 해당하는 글 1건

번역자 : 위 글은 2년전(2014년)에 쓰여진 글임에도 번역한 이유는, 이 개념을 알면 수월하고 모르면 해맬 수 있기 때문입니다. 그렇게 대단한 이야기는 아닐지 모르지만 부모와 자식 뷰 간의 터치 이벤트를 주고 받는 일이 있다면 꼭 한번 읽어보길 추천합니다. 


Hit-Test은 한 점(터치 포인트와 같은)이 화면에 그려진 그래픽적인 오브젝트(UIView와 같은)를 관통하는지 결정하는 프로세스이다. 가장 앞쪽에 올라와 있는 view가 무엇인지 알아내기 위해 iOS에서는 Hit-Test라는 것을 사용한다.  Hit-Test는 역방향 우선순위 깊이우선 탐색(Reverse Pre-Order Depth-First Travesal) 알고리즘을 이용하여 view 계층을 조사하도록 구현되어 있다.

Hit-Test가 어떻게 동작하는지 설명하기 전에,  Hit-Test이 언제 호출되는지부터 알아보자. 아래 다이어그램은 손가락이 화면에 터치되는 순간부터 다시 들어올리는데까지 터치 과정의 큼직큼직한 플로우(high-level flow)를 보여준다.


위 다이어그램에서 보여주듯, 화면을 터치하는 매 시간마다  Hit-Test가 호출된다.  Hit-Test 이전 시점에는 어떤 view나 gesture recognizer가 UIEvent 객체를 받는데, 터치가 어느 지점에서 됬는지 그 event에 표현된다.

Note : 알 수 없는 이유로 Hit-Test가 한번에 여러번 호출된다. 이미 결정된 Hit-Test view도 비슷한 현상이 일어난다.

Hit-Test가 끝나고 나면 터치 포인트 아래 최상단 view가 결정된다. 그  Hit-Test view는 터치 이벤트 순서(began, moved, ended, cancelled)의 모든 UITouch 객체와 연관되어있다. 추가적으로  Hit-Test view에서 view나 조상 view들에 붙은 어떤 gesture recognizer들도 위의 UITouch와 연관되어있다. 그러면  Hit-Test view는 터치 이벤트를 순서대로 받기 시작한다. 한가지 염두하고 있어야 할 것은 손가락을 움직여  Hit-Test view의 범위(bounds)를 넘어 다른 view로 움직여도  Hit-Test는 터치 이벤트 순서가 끝날 때까지 모든 touch들을 받고 있을 것이다.

" 그 Touch 객체는 후에 터치가 view 바깥으로 움직여 나갔더래도 그 라이프 타임 동안에는  Hit-Test view와 연관되어있다."

앞에서 말했듯  Hit-Test은 역방향 우선순위(Reverse Pre-Order)의 깊이우선 탐색(Depth-First Travesal)을 사용한다. (먼저 root 노드를 탐색하고 그 다음 높은 index에서 낮은 index 순서로 자식 노드를 탐색한다.) 이런 종류의 탐색은 반복적인 탐색을 줄여주고, 터치포인트가 포함된 view를 내림차순으로 깊이 우선으로 검색하다가, 결과물을 찾으면 찾는 과정을 멈춘다. 이것이 가능한 이유는 subview가 항상 superview보다 위에서 그려지고(render), sibling(형제) view는 subviews 배열 안에서 index가 낮은 sibling view 보다 위에 그려지기 때문이다. 따라서 특정 포인트에 여러 view가 겹쳐져 있으면, 가장 깊은 view 중에 가장 오른쪽 view가 최상단 view가 될 것이다.

"시각적으로 subview의 요소는 subview의 parent view의 모든것을 가리거나 일부분을 가린다. 각 superview는 순서를 가지는 배열로 그것의 subview들을 가지는데, 그 순서는 각 subview의 상태에 따라 배열에 영향을 준다. 만약 두 sibling subview가 서로 겹쳐있다면 마지막에 추가된 subview가 다른 subview 위쪽에 나타나 있을 것이다."

아래 다이어그램은 view 계층 tree에 관한 예제이고, 화면에 그린 UI에 매칭시켜 놓은 것이다. 왼쪽에서 오른쪽으로 정렬된 tree branch들은 subviews 배열의 순서를 의미한다.


위에서 볼 수 있듯이, View A의 자식인 View A.2와 View B의 자식인 View B.1은 서로 겹친다. 그러나 "View B"의 subview index가 "View A"보다 크므로 View B와 그 subview들은 View A와 그것의 subview들보다 위에 그려진다. 그러므로  사용자의 손가락이 View B.1과 View A.2가 겹치는 부분을 터치하여 Hit-Test를 하게되면 View B.1이 반환된다. 

Depth-First Travesal in Reverse Pre-Order 방식을 적용함으로써 가장 깊은 내림차순으로 터치 포인트가 포함된 view를 찾으면 탐색을 멈춘다.


이 탐색 알고리즘은 UIWindow에 hitTest:withEvent: 메시지를 보냄으로서 시작되는데, UIWindow는 view 계층에서  root view이다. 이 메소드로부터 반환되는 값(view)은 터치 포인트를 포함하는 최상단 view이다.

아래 플로우 차트가  Hit-Test 로직을 설명한다.


그리고 아래 코드는 원래 hitTest:withEvent: 메소드를 실제 구현해본 것이다.


hitTest:withEvent: 메소드는 먼저 터치를 받을 수 있는지부터 확인한다.

view가 터치를 받을 수 있다면:
  • view가 hidden 이 아니여야한다.
    self.hidden = NO;
  • view의 userInteraction이 enable이여야한다.
    self.userInteractionEnable == YES;
  • view의 alpha가 0.01보다 커야한다.
    self.alpha > 0.01
  • view가 포인트를 포함해야한다.
    pointInside:withEvent == YES
그러고 view가 터치 받는 것을 허락하면, 이 메소드는 어떤 어떤 하나가 nil을 반환하기 전까지 높은 곳에서 낮은 곳으로 그것의 각 subview에 hitTest:withEvent: 메시지를 보냄으로서 receiver의 subtree를 탐색한다. 그 subview들에의해 반환된 첫번째 nil이 아닌 값은 터치 포인트를 포함하는 최상단 뷰이고 receiver에의해 반환된다. 만약 모든 receiver의 subview들이 nil을 반환하거나 그 receiver가 suview가 없으면 자기 자신을 반환한다.

다르게말하면, view가 touch를 받지 못하도록 되있을 때는 이 메소드가 더이상 receiver의 subtree를 탐색하지 않고 nil을 반환한다. 그러므로 이  Hit-Test 작업에서는 모든 view들의 계층을 다 탐색하지 않아도 된다는 것이다.

hitTest:withEvent:를 override 하여 사용한 일반적인 유스케이스
한 view가 터치 이벤트 매 순간마다 다른 view에 리다이렉트될 수 있도록 터치 이벤트를 다룰 경우 hitTest:withEvent: 메소드를 override 하면 된다.

" Hit-Test가 호출되기 전에 터치 이벤트 순서 중 첫번째 터치 이벤트가 그것의 receiver에게 보내기 때문에, 이벤트들을 리다이렉트하기위해 hitTest:withEvent:를 override 하는 것은 그 이벤트 순서의 모든 터치 이벤트를 리다이렉트 하게 될 것이다."

View의 터치 면적 넓히기
hitTest:withEvent: 메소드를 override 할 수 있는 또하나의 경우는 view의 터치 범위가 그것의 실범위(bounds)보다 커야할 때 이다. 예를들어, 아래 그림은 20X20 크기의 UIView를 나타낸다. 이 크기는 실제 손가락으로 touch 받기엔 너무 작다. 따라서 그것의 터치 면적을 hitTest:withEvent: 메소드를 override하여 각 방향마다 10px씩 증가시킬 수 있다.



Note : 이 view에대해 정확하게  Hit-Test 하기위해, parent view의 영역(bounds)은 child view의 원하는 터치영역을 포함하고 있거나, 원하는 터치 영역을 포함하기 위해 그것의 hitTest:withEvent: 메소드를 overrid 해야한다.

아래에 터치 이벤트들을 view들에 통과시키기
종종 해당 view의 터치 이벤트를 무시하고 그 view의 아래에 통과시켜야 할 때가 있다. 예를들어, 앱 위에 투명하게 전체적으로 view가 덮혀올라간 경우를 생각해보자. 이 오버레이 는 평범하게 터치를 받을 수 있는 control들과 button들의 subview들을 가질 것 이다. 그러나 어디에서나 오버레이를 터치하면 그 오버레이 아래의 view들에게 터치 이벤트를 넘겨줄 수 있다. 이 동작을 완료하기 위해서는 오버레이에서 터치포인트를 포함하는 그것의 subview들 중 하나를 반환하거나 nil을 반환하기 위해 hitTest:withEvent:를 override할 수 있다. 오버레이 위에 터치가 된 경우에도 마찬가지이다.


subview에 터치이벤트 보내기
또 다른 경우는 parent view의 모든 터치 이벤트를 child view로 보낼때이다. child view가 parent view의 일부분만을 차지하지만 그 parent에서 발생하는 모든 터치들에 반응해야할때 이 동작이 반드시 필요하다. 예를들어, 이미지들이 회전목마처럼 구현된 UI를 생각해보자. parent view로는 UIView, 그 위에 child view로는 UIScrollView를 가지고 이 view는 pagingEnable를 YES로 clipsToBounds를 No로 설정한다. 그 위에 이미지들을 올려 구성한다.


UIScrollView의 내부만 터치를 받는것이 아니라 외부에도 터치를 받고 싶은데, 그 scroll view의 parent view 범위 내로 제한하고 싶을때, 아래와 같이 그 parent의 hitTest:withEvent: 메소드를 override 함으로서 가능하다.





WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,