[ swift ] hitTest에 대하여

sonny·2024년 12월 1일
1

TIL

목록 보기
58/133

hitTest란?

공식문서)

hitTest(_:with:)의 역할을 보자면,

hitTest(_:with:)는 뷰 계층에서 어떤 뷰가 터치 이벤트를 처리할지 결정하는 메서드이다.

point는 터치된 지점이고, event는 발생한 터치 이벤트 정보를 나타내주는데,

이 메서드는 호출된 뷰나 해당 뷰의 자식 뷰들 중에서 터치 이벤트를 처리할 적합한 뷰를 반환한다는 것이다.

터치 이벤트를 처리할 뷰를 터치 대상 뷰(hit view)라고 한다.

비유로 설명하자면,

터치 이벤트를 발생킨다고 칠 경우 iOS는 "이 터치를 누가 처리할까?"를 고민하게 된다.

그래서 hitTest(_:with:)를 호출해 "가장 적합한 뷰를 찾는 과정"을 거치고,

이 과정은 마치 "터치된 위치에 가장 가까운 사람(뷰)을 찾는 것"과 비슷하다고 생각하면 된다.

작동 방식

먼저 터치된 점(Point)이 주어지는데,

iOS는 뷰의 계층 구조를 따라 터치된 지점에 있는 가장 안쪽의 뷰(Subview)를 찾게 된다.

그 뷰가 이벤트를 처리할 수 있다면 해당 뷰를 반환하는 것이고,

그렇지 않다면 부모 뷰로 돌아가면서 적합한 뷰를 찾게 된다.

왜 사용하나?

  • 특정 뷰에서만 터치 이벤트를 처리하고 싶을 때
    ex) 화면의 일부만 스크롤되도록 제한 할 경우.

  • 터치 이벤트를 다른 뷰로 넘기고 싶을 때
    ex) 투명한 뷰가 이벤트를 무시하고 부모 뷰로 전달.


hitTest를 사용하게 된 이유

그렇다.

내가 테이블뷰의 높이를 0으로 하더라도 터치 영역이 남아 있을 수 있다는건다,

UIView나 그 서브클래스(UITableView)는 프레임(Rect)의 넓이나 높이가 0이라도

해당 위치에 존재한다면 터치 이벤트가 전달될 수 있다는 것이다.

그러니까 프레임의 크기는 작더라도 뷰가 완전히 제거되거나 숨겨지지 않는 한 여전히 터치 이벤트 처리 대상인 셈.


hidden과 높이 0의 차이

hidden = true

  • 해당 뷰는 화면에서 보이지 않지만, 터치 이벤트는 여전히 처리될 수 없다. 실제로 isHidden = true일 때 해당 뷰는 hitTest(_:with:)에서 제외되고,
    부모 뷰로 이벤트가 전달되는데 그래서 hidden이 true일 때는 터치 이벤트가 그 뷰에 영향을 미치지 않는다.

높이 0(height = 0)

  • 화면에서 뷰가 렌더링되지 않지만 여전히 hitTest 메서드에서 처리될 수 있다.
    그러나 높이가 0이라도 그 뷰가 터치 이벤트를 받게 될 수 있는 경우는 isHidden = false일 경우다.

나는 테이블뷰를 초반에 숨기고 싶었기 때문에 hidden = true 로 진행했고,

높이도 처음에 0으로 설정해주었다.

그럼 당연히 숨긴 부분은 사용하지 않고 있을 때 터치가 되어야하는게 맞잖아....


isHidden = true의 영향

알고보니 isHidden = true를 설정하면 해당 뷰는 화면에서 보이지 않는건 맞지만,

여전히 frame(크기와 위치)는 유지된다고 한다.

뷰의 크기나 위치는 0이라 하더라도 여전히 이벤트를 처리하려고 할 수 있다는데,

isHidden = true일 때는 기본적으로 해당 뷰가 터치 이벤트를 받을 수 없도록 처리되는 것이고,

여기서 스크롤이 되지 않는 이유는 tableViewheight = 0으로 설정돼서

스크롤 콘텐츠 자체가 화면에 표시되지 않거나 이벤트가 해당 뷰로 제대로 전달되지 않아서일 가능성이 크다고 한다.

height = 0의 영향

height = 0으로 설정된 뷰는 화면상에서 실제로 존재하지 않는 것과 동일한 효과를 낳지만,

나는 isHidden = true와 함께 사용했기 때문에 뷰는 화면에 그려지지 않았다.

그래서 여전히 메모리 상에 존재하고 있던 것이고,

그 크기(높이 0)가 스크롤 이벤트에 영향을 줄 수 있던 것.

tableView높이가 0이면 스크롤 가능한 콘텐츠가 존재하지 않아서 스크롤 동작이 실제로 표시되는 콘텐츠에 대해서만 동작하게 되는데,

스크롤 뷰는 contentSize에 따라 동작하기 때문에 height = 0일 때는 스크롤이 동작할 콘텐츠가 없어서 제대로 스크롤되지 않을 수 있다는 설명이 될 수 있다.


결론적으로 정리하자면,

isHidden = true 를 사용하면 뷰는 화면에 보이지 않으며 터치 이벤트를 차단하게 되지만,

해당 뷰의 크기와 위치는 여전히 존재해서 그 영역을 차지하게 되기 때문에

이벤트가 발생할 수 있는 영역이 그대로 남아 있을 수 있다는 것.

그럼에도 불구하고 터치 이벤트는 차단되니까 사용자는 해당 뷰를 터치할 수 없고, 이벤트는 다른 뷰로 전달되는 것이다.


hitTest 사용한 이유

hitTest를 사용해서 터치 이벤트가 테이블 뷰로 전파되지 않도록 설정한 이유는,

tableView.isHidden = trueheight = 0으로 뷰가 화면에 나타나지 않거나 이벤트를 받을 수 없게 만들어도,

여전히 터치 이벤트가 부모 뷰에 전달될 수 있기 때문이다.

그래서 hitTest를 오버라이드해여 이벤트가 다른 뷰로 전달되지 않도록 제어한 것.


왜 스크롤이 차단됐었을까

테이블 뷰와 커스텀 뷰가 합쳐지면서 height를 0으로 설정했지만,

이벤트 전달 과정에서 논리적 충돌이 발생한 것이라는 걸 짐작해볼 수 있다.

설명했다시피 테이블 뷰의 프레임은 0이지만, 터치 이벤트는 여전히 처리 대상이 되었기 때문에 스크롤 동작이 차단되고,

커스텀 뷰의 hitTest를 통해 터치 이벤트를 명확히 처리하지 않으면,

스크롤 이벤트와 터치 대상이 제대로 설정되지 않기 때문..


작성코드

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !tableView.frame.contains(point) else {
        return super.hitTest(point, with: event)
    }
    return nil
}

tableView.frame.contains(point)

point는 터치된 지점의 좌표인데 그 지점은 tableView.frame 테이블 뷰의 프레임이다.
그래서 frame.contains(point)는 터치된 지점이 tableView의 프레임 내부에 있는지 확인하는데,
결과가 true인 경우는 터치가 tableView 내부에서 발생한 것이라 볼 수 있고
결과가 false인 경우는 터치가 tableView 외부에서 발생한 것이라 볼 수 있다.

guard !tableView.frame.contains(point)

이 조건을 걸은 건,
터치가 tableView 외부에서 발생했을 때만 아래 코드를 실행하도록 하는데,
else 부분에서 super.hitTest(point, with: event)를 호출해 부모 뷰의 기본 동작을 수행하도록 한 것이다.

super.hitTest(point, with: event)

기본적으로 부모 뷰의 hitTest 메서드를 호출해 이벤트 처리를 진행하게 되고,
이 코드가 실행되면 터치 이벤트는 tableView가 처리할 수 있게 된다.

return nil

nil을 반환하면 터치 이벤트가 처리되지 않고, 이벤트는 더 이상 전달되지 않는다.
이 경우는 tableView 외부에 터치가 발생하면 아무런 반응이 없게 된다.


결과

업로드중..

업로드중..

테이블뷰 영역에서도 스크롤이 원활하게 작동하는 걸 볼 수 있다.

라며 gif 를 똑같이 올리는데 자꾸 이미지 업로드가 실패한다...

나중에 업로드 꼭 해야지.

아무튼 스크롤이 테이블뷰 영역 안에서도 잘 되는걸 확인했다.


음...

테이블뷰 영역에 스크롤이 되지 않는 버그를 팀원이 발견하고 해결방법을 알아보았지만

도무지 되지 않아 튜터님에게 질문하여 알게 된 메서드였다.

사실 그 당시 나는 이해가 잘 가지 않아서 아 hitTest라는 메서드가 그냥 최상단의 뷰를 터치해주는 메서드라고 간단하게 생각하고 넘어갔는데,

이 부분이 우리 아이맥도날드에서 나름 중요하게 쓰인 메서드라고 생각이 들어 다시 공부하고 싶었다.

오늘 hitTest에 대해 공부하면서 터치 이벤트가 어떻게 뷰 계층에서 처리되는지에 대해 중요한 개념들을 이해할 수 있었다.

hitTest는 화면에서 터치가 발생했을 때 그 터치가 어떤 뷰에 전달될지 결정하는 메서드인 것이고,

터치 위치를 기준으로 적합한 뷰를 찾아 반환한다는 점을 알게 됐다.

isHidden = true와 같은 속성이 뷰에 적용되었을 때,

해당 뷰가 보이지 않지만 여전히 크기와 위치를 차지하고 있기 때문에

터치 이벤트가 그 뷰에 영향을 미칠 수 있다는 점도 이해할 수 있었고,

이때 hitTest를 오버라이드하면 터치 이벤트를 명시적으로 처리할 수 있다는 점이 정말 유용하다 생각했다.

전반적으로 hitTest는 화면 상에서 터치가 발생할 때 그 터치를 받을 뷰를 결정하는 중요한 역할을 한다는 걸 알았으니 ..

이건 앞으로의 과제를 수행하면서도 꼭 기억해야할 메서드라는 것을 느꼈다.

profile
iOS 좋아. swift 좋아.

2개의 댓글

comment-user-thumbnail
2024년 12월 1일

오,, 이번 프로젝트에서 진짜 많은 메서드를 사용해보셨네요
미리 테이블 뷰도 공부해두고 멋지네

1개의 답글

관련 채용 정보