티스토리 뷰

iOS

iOS ) hitTest

Zedd0202 2018. 6. 13. 19:25
반응형

안녕하세요 :) Zedd입니다. 

라이브러리를 사용하면서 소스 보면 가아끔 hitTest가 있었는데, 뭐지?하고 그냥 지나쳤던 기억이...

오늘 제대로 공부해볼려고 해용

이를 위해서..UIResponder를 썼었죠..



hitTest




UIView의 인스턴스 메소드입니다.


정의는 


"Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point.


지정된 점(point)을 포함하는 View 계층(자신 포함)에서 수신기(..?receiver그냥 리시버라고 영어로 하는게 가장 이해가 잘될듯)의 가장 먼 자손(farthest descendant)을 리턴합니다


네...그렇습니다...^^역시 Apple은 정의만 보면 무슨소린지 모른다는게 아주 재밌죠

View계층에서 가장 가까운 자손도 아니고 가장..먼...farthest... descendant..를 리턴한다니..


아직 무슨소리인지 모르는게 당연합ㄴ디ㅏ. 

더 자세히 봅시당



메소드의 원형은 이러합니다.

CGPoint타입의 파라미터 point와 UIEvent타입의 event를 받네요.


point : 리시버의 로컬 좌표계(bounds)로 지정된 점(point)

event : 이 메소드에 대한 호출을 보증(warranted. 보증하다, 단언하다, 장담하다 등)하는 이벤트입니다. 이벤트 처리코드 외부에서 이 메소드를 호출하는 경우에는 nil을 지정 할 수 있습니다.


리턴타입은 UIView?네요! 정의에서 View계층에서 가장 먼 자손을 리턴한다 했으니..그 자손은 당연히 UIView계층에 있을 테고, 당연히 UIView타입일것입니다. 없을 수도 있으니 옵셔널로 되어있나보네요.

리턴에 대한 자세한 설명을 봅시다.

현재 View의 가장 먼 자손이며, 점(point)를 포함하는 View객체입니다. 점(point)이 리시버의 View계층 외부에 있으면, nil을 반환합니다.

정의만 보면 모르지만 Discussion을 보고 나면 아주 살짝 감이 오게되죠. ㄱㄱ

Discussion

이 메소드는 각 하위 View의 point(inside:with:) 를 호출하여 View계층을 탐색하고, 어떤 하위 View가 터치 이벤트를 받아야 하는지 결정합니다. 
※ 여기서 잠깐...point(inside:with:) 를 잠깐 알아봅시다. 역시나 UIView의 인스턴스 메소드이며, 뭔가 메소드 파라미터를 보면 감이 오지 않으시나요!? 리시버에 지정된 point가 포함되어 있는지 여부를 나타내는 Bool값을 반환하는 메소드입니다.

HitTest 마찬가지로 point event 받네요?

HitTest point event 완전히 동일합니다. 

다른건 리턴값인데, 정의를 보면 Bool값을 리턴한다는 것을 있죠. 

point 리시버 bounds안에 있으면 true 아니면 false 리턴합니다.



계속 hitTest 볼게요.


point(inside:with:) true 반환하면, 하위 View계층구조는 지정된 (point) 포함하는 가장 앞에 있는 View(frontmost view) 발견 까지 하위 View계층 구조가 비슷하게 가로지릅니다.(similarly traversed) 

View (point) 없으면, View계층 구조의 해당 분기가 무시됩니다.

메소드는 직접 호출할 필요가 거의 없지만, 하위 View에서 터치 이벤트를 숨기려면 메소드를 재정의 있습니다.

메소드는 숨겨져 있거나, 사용자 상호작용을 비활성화 하거나, 알파값이 0.01 미만인 View객체를 무시합니다. 메소드는 “hit” 결정할 , View 컨텐츠를 고려하지 않습니다. 따라서 지정된 (point) 해당 View 컨텐츠의 투명한 부분에 있어도 View 계속 반환 있습니다. 

리시버 bounds외부에 있는 (point) 실제로 리시버 하위 View 하나에 속하더라도, hit으로 보고되지 않습니다. 문제는 현재 View clipsToBounds 프로퍼티가 false 설정되고, 영향을 받는 하위 View 범위를 벗어나는 경우 발생할 있습니다. 





와ㅏㅏ Discussion을 봐도 잘 모르겠죠?

가장 먼 자손..farthest descendant은...뭘 리턴한다는 것인지..

문서가 조금 어렵게 되어있는 것 같습니다... Discussion..보고나면,...이해할 줄 알았지..


- 여기까지 읽고 제가 헷갈렸던 부분

바로. farthest descendantfrontmost view입니다.


farthest...

far가 멀다인데 est가 붙었으니까 가장 “먼”거잖아요?

이 farthest는 

"Returns the farthest descendant of the receiver

 리시버의 가장 먼 자손을 리턴한다는 hitTest(_ :with:)의 정의에 있습니다.  


근데 Discussion에는 이런말이 또 나오죠. 

point(inside:with:) 가 true를 반환하면, 하위 View계층구조는 지정된 점(point)을 포함하는 가장 앞에 있는 View(frontmost view)가 발견 될 때 까지 하위 View계층 구조가 비슷하게 가로지릅니다.(similarly traversed) 


지정된 점을 포함하는 가장 앞에 있는 view(frontmost view)가 발견될때까지~


띠용

;;;

근데 이게 음 제가 생각하기엔, 누가 바라보고 있냐? 누가 부모냐..? 이걸 뭐라해야되지

하위 View계층 구조에서 지정된 점을 포함하는 가장 앞에 있는 view는 내가 지금 touch를 한 view에서 바라보면 지정된 점을 포함하는 가장 먼 view인 것이죠.


제가 이해하는게 맞는지는 모르겠는데...hit test에 대한 내용이 왜이렇게 없는건지 ㅠㅠㅠ

아마 제가 생각한게 맞다면.....한번만 곰곰이 생각하시면 이해가 되실거에요.

 farthest descendant와 frontmost view.


ㅇㅋ 




암튼 다음으로.

 

자, 그럼 해당 점을 포함하는... (내가 터치를 누른 view에서부터) 가장 먼 view를 찾았어.

찾으면 도대체 뭘하는것이냐;;

touch event 시퀀스의 모든 단계(began, moved, ended, canceled)에 대한 UITouch객체와 연동됩니다. 

그런 다음에, hit-test view가 일련의 touch event를 받기 시작하는 것이죠.


꼭 기억해야할 점은, 손가락이 hit-test view의 bounds를 넘어 다른 view로 가도, hit-test view는 touch event 시퀀스가 끝날때까지(ended가 올때까지?) 계속 모든 touch를 수신합니다. 

 

아하 이제 나오네요. 아까 헷갈렸던 부분..

그 전에 알고있어야할게 하나 있는데, 이 view를 탐색할 때, reverse pre-order DFS(depth-first traversal)탐색방법을 사용한다는 것입니다.


처음에는 루트를 방문하고, 다른 상위 트리에서 하위 인덱스로 하위 트리로 이동하는 방식이죠. 이러한 탐색은 touch point를 포함하는 가장 먼 view가 발견되면, 탐색 반복 횟수를 줄이고, 검색 프로세스를 중지 할 수 있습니다.


이것은 subview가 superview “앞에” 렌더링되고, 형제view가 항상 subview배열에서 더 낮은 인덱스로, 형제 view앞에 렌더링되므로 가능합니다. 여러개의 겹치는view에 특정 point가 포함되어 있는 경우, 가장 오른쪽 서브트리의 가장 먼 view가 hit-test view가 됩니다.


이게 무슨.......소리야....싶으실건데,

그림으로 일단 볼게요.




왼쪽의 그림을 봐주세요. 

MainView인 가장 뒤에 하얀색 view가 있고, 그 위에 3개의 view가 있네요.

MainView입장에서는 3개의 view모두 자기 자식입니다.


자, 위 그림에서 볼 수 있듯이 ViewA는 빨간색, ViewB는 초록색입니다. ViewC는 회색이네요.

ViewA와 ViewB가 겹쳐있는 것을 볼 수 있습니다.

정확히 말하면,

ViewA의 subView인 ViewA.2와 ViewB의 subView인 ViewB.1이 겹쳐있죠.

하지만, ViewB는 ViewA의 subView index보다 높기 때문에, ViewB는 ViewA위에 렌더링 되게 됩니다.

그림에서도 볼 수 있듯이 초록색view가 빨간색view앞에 있는 걸 볼 수 있죠. 

그럼 저 ViewA.2와 ViewB.1이 겹치는 부분을 터치하게 되면, 가장 전면에 있는, frontmost인 view인 ViewB.1가 반환이 될 거겠네요.





오른쪽 그림을 보면, 역방향인것알 수 있죠? 신긴한건, ViewC도 들른다는 점...

ViewC는 해당 point를 포함하지 않고 있기 때문에, false를 리턴했을테고, 바로 ViewB로 넘어온것을 볼 수 있네요. 

결국 ViewB.1이 hit-test view가 됩니다.

정말로 가장 먼 자손이죠? 

지금 이 view계층에서요.



- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 || ![self pointInside:point withEvent:event] || ![self _isAnimatedUserInteractionEnabled]) {

        return nil;

    } else {

        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {

            UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];

            if (hitView) {

                return hitView;

            }

        }

        return self;

    }

}


자, hittest의 구현부입니다.

위 discussion에서 보았듯이.. hidden이거나, interact가 false이거나, alpha가 0.01미만인건 nil을 반환합니다.


그럼 뭔가 대충 hitTest에 대해서 감이 오시죠 이제..!!

안오실 수도 있는데, 설명을 계속계속 읽으면 조금씩 감이 옵니다.


그래서 이 hitTest를 제가 공부하게 된 이유...바로 Touch event의 전달때문이죠.



음..예를들어 위 그림처럼 View들이 있다고 생각해볼게요.

저 부분을 touch 하면, View B.1가 반환될 것입니다.

View A.2가 해당 지점(point)를 포함하고 있어도 말이죠.


그럼 이럴 수 있겠죠. 

나는 View B.1을 touch했지만, View A.2가 touch된 걸로 하고싶어!!

== View A.2가 touch event를 받는 hit-test view가 되게 하고싶어!!!


이럴때, hitTest(_:with:)메소드를 override할 수 있습니다.


제가 정말...소소한...레알루다가 소소함...

한 예제를 들고왔는데요,


이런 상황이 있다고 칩시다.


띠용


저렇게 tableView앞에 view가 있으면..


화남.



왜냐면 스크롤이 안되니까

== 

저 초록색 view가 touch event를 받으니까 

==

저 view가 저 지점?point에서의 hit-test view니까


ㅡㅡ

근데 굳이 저 초록색 view를 저기에 꼭 놔둬야 하고..스크롤이 되어야 한다면......................

이럴때 hitTest를 override하는 것입니다.


내가 이 touch event를 받는 view가 맞지만..!!! 맞지만!!!!!!!!! nil을 리턴해준다면???

네 해당 view보다 하위에 있는 view로 touch event가 넘어가게 됩니다.


ㄱㄱ


class ZeddView: UIView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let hitView: UIView? = super.hitTest(point, with: event)

        if (self == hitView) return nil }

        return hitView

    }

}

커스텀 UIView클래스를 만들어, hitTest를 override해줍니다.

코드는 어렵지 않죠? 그 hitTest를 받는 애가 나다!!!!!!!!!!!!!라고 하면 nil을 리턴해주면 자연스럽게 하위에 있는 view로 touch event가 넘어가겠죠. 

제가 든 예제에서는 그 하위view가 tableView가 되겠구요.


이렇게 만들어주고 ㅎㅎ




저 해당 view의 클래스를 방금 만든 커스텀 UIView클래스로 해주고 실행하면




touch이벤트가 잘~~넘어간 것을 볼 수 있습니다.

아무튼...지금 예제는 이렇지만, 다양한 상황에서 사용 할 수 있겠죠? 


드디어 조금조금씩 쓰던..HitTest글을 마무리하네요.

hitTest가 어떤 원리인지 정확히 알게되어서 저도 넘나 기쁜것

틀린점이나 지적할 부분이 있다면 댓글이나 PC화면 오른쪽 하단의 채널서비스를 이용해주세요 :)

hitTest를 이해하는데 도움이 되었길 바랍니다 XD..


출처 : 

http://smnh.me/hit-testing-in-ios/

https://developer.apple.com/documentation/uikit/uiview/1622469-hittest?language=objc

반응형

'iOS' 카테고리의 다른 글

iOS ) windowLevel  (0) 2018.07.28
iOS ) StoreKit  (10) 2018.06.25
iOS ) UIApplication  (3) 2018.05.27
iOS ) UIResponder  (5) 2018.05.26
iOS ) AVKit과 AVFoundation  (1) 2018.05.13