꿈돌이랜드

[iOS] hitTest 이해하기 본문

Programming/iOS

[iOS] hitTest 이해하기

loinsir 2023. 6. 14. 15:05
반응형

지난번 글의 Responder Chain과 연관해서 알아두어야 할 hitTest 입니다.

애플 문서를 기반으로 작성했습니다.

hitTest(_:with)

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

자신을 포함한 현재 뷰의 뷰 계층에서 지정된 point를 포함하는 가장 먼 자손을 리턴합니다.

func hitTest(
    _ point: CGPoint,
    with event: UIEvent?
) -> UIView?
  • point: 뷰의 로컬 좌표계에 있는 지점
  • event: 이 메서드에 대한 호출을 보장하는 이벤트. 이벤트 처리 코드 외부에서 이 메서드를 호출하면 nil을 반환합니다.

반환값

현재 뷰의 가장 먼 하위 뷰이며 point를 포함하는 뷰 객체 입니다. point가 현재 뷰의 뷰 계층 구조 외부에 있는 경우 nil을 반환합니다.

Discussion

이 메서드는 터치 이벤트를 보낼 하위 뷰를 결정하기 위해 각 하위 뷰의 point(inside:with:)메서드를 호출해서 뷰 계층 구조를 순회합니다. 만약 point 메서드가 true를 리턴한다면, 이 메서드는 특정 point를 포함하는 가장 앞의 뷰를 찾을 때까지 계속해서 하위 뷰의 계층을 순회합니다. 만약 뷰가 더 이상 point를 포함하지 않는다면, 이 메서드는 뷰 계층의 밑 가지들을 무시합니다. 이 메서드를 직접 호출할 필요는 거의 없을 테지만, 오버라이딩 해서 서브 뷰 로부터 터치 이벤트를 숨기도록 할 수 있습니다.

이 메서드는 hidden 되었거나, disabled user interactions 되었거나, 혹은 alpha 값이 0.01 보다 작은 경우 등의 뷰를 무시합니다. 이 메서드는 hit를 결정할 때 뷰의 컨텐츠를 고려하지 않으므로, 지정된 지점이 해당 뷰의 컨텐츠의 투명한 부분에 있더라도, 뷰를 반환할 수 있습니다.

이 메서드는 뷰의 하위 뷰 중 하나에 실제로 있는 경우에도 뷰의 범위 밖에 있는 지점을 hit로 보고하지 않습니다. 이 상황은 뷰의 clipToBounds 프로퍼티가 false 이고, 뷰의 경계를 넘어 서브뷰가 확장된되는 경우에 발생할 수 있습니다.

정리

사용자가 터치를 수행한 경우, UIKit은 touch 이벤트 시퀀스의 모든 단계에 대해 UIEvent를 생성하고, hit test 뷰가 일련의 touch event를 받기 시작합니다.

중요한 점은, touch 객체가 생명주기 동안, touch가 view 바깥으로 벗어나 이동하더라도, hit test 뷰와 계속해서 연관되어진다는 것입니다.

hit test를 수행할 때, reverse pre-order DFS로 뷰 계층을 탐색합니다. 처음에는 루트를 방문하고, 이후 상위 트리에서 다시 하위 인덱스로 하위 트리로 이동하는 방식입니다. 이러한 탐색은 touch point를 포함하는 가장 먼 view 를 찾았을 때, 탐색을 중단시킬 수 있고, 탐색 반복 횟수를 줄일 수 있습니다.

위 그림에서 View A보다 VIew B가 z index가 높기 때문에, View B는 View A위에 렌더링 됩니다. 이 상황에서 만약 View A.2와 View B.1이 겹치는 지점을 터치하게 되면 가장 전면에 있는 View B.1이 반환되게 됩니다.

View 계층을 reverse pre-order DFS로 순회하므로, View C 역시 탐색하게 됩니다. 하지만, View C의 경우 해당 지점을 포함하고 있지 않기 때문에 false를 반환하고, View B를 탐색하게 되어, View B.1이 hit test view가 되게 됩니다.

아래는 hitTest의 구현부 입니다. 애플 문서와 마찬가지로, hidden, userInteractionEnabled 값이 true, alpha 값이 0.01 미만 등의 경우 nil을 반환하고, 그렇지 않다면 서브뷰를 순회합니다.

- (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 메서드를 오버라이드 하여 Touch event의 전달을 임의로 조절할 수 있습니다.

class CustomView: 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
    }
}

위 코드는 해당 뷰 아래 z index의 뷰로 touch Event를 넘기도록 설정하는 뷰의 예시입니다.

https://developer.apple.com/documentation/uikit/uiview/1622469-hittest https://smnh.me/hit-testing-in-ios


Uploaded by N2T

반응형

'Programming > iOS' 카테고리의 다른 글

코코아 인터널스 - 2장 메모리 관리  (0) 2023.08.07
코코아 인터널스 - 1장  (0) 2023.07.23
[iOS] 앱 Test 하기  (0) 2023.07.08
[iOS] Responder와 Responder Chain 이해하기  (0) 2023.06.13
N2T 테스트 페이지  (0) 2023.05.01