공식문서)
hitTest(_:with:)
의 역할을 보자면,
hitTest(_:with:)
는 뷰 계층에서 어떤 뷰가 터치 이벤트를 처리할지 결정하는 메서드이다.
point
는 터치된 지점이고, event
는 발생한 터치 이벤트 정보를 나타내주는데,
이 메서드는 호출된 뷰나 해당 뷰의 자식 뷰들 중에서 터치 이벤트를 처리할 적합한 뷰를 반환한다는 것이다.
터치 이벤트를 처리할 뷰를 터치 대상 뷰(hit view)라고 한다.
터치 이벤트를 발생킨다고 칠 경우 iOS는 "이 터치를 누가 처리할까?"를 고민하게 된다.
그래서 hitTest(_:with:)를 호출해 "가장 적합한 뷰를 찾는 과정"을 거치고,
이 과정은 마치 "터치된 위치에 가장 가까운 사람(뷰)을 찾는 것"과 비슷하다고 생각하면 된다.
먼저 터치된 점(Point)이 주어지는데,
iOS는 뷰의 계층 구조를 따라 터치된 지점에 있는 가장 안쪽의 뷰(Subview)를 찾게 된다.
그 뷰가 이벤트를 처리할 수 있다면 해당 뷰를 반환하는 것이고,
그렇지 않다면 부모 뷰로 돌아가면서 적합한 뷰를 찾게 된다.
특정 뷰에서만 터치 이벤트를 처리하고 싶을 때
ex) 화면의 일부만 스크롤되도록 제한 할 경우.
터치 이벤트를 다른 뷰로 넘기고 싶을 때
ex) 투명한 뷰가 이벤트를 무시하고 부모 뷰로 전달.
그렇다.
내가 테이블뷰의 높이를 0으로 하더라도 터치 영역이 남아 있을 수 있다는건다,
UIView나 그 서브클래스(UITableView)는 프레임(Rect)의 넓이나 높이가 0이라도
해당 위치에 존재한다면 터치 이벤트가 전달될 수 있다는 것이다.
그러니까 프레임의 크기는 작더라도 뷰가 완전히 제거되거나 숨겨지지 않는 한 여전히 터치 이벤트 처리 대상인 셈.
hidden = true
높이 0(height = 0)
나는 테이블뷰를 초반에 숨기고 싶었기 때문에 hidden = true 로 진행했고,
높이도 처음에 0으로 설정해주었다.
그럼 당연히 숨긴 부분은 사용하지 않고 있을 때 터치가 되어야하는게 맞잖아....
알고보니 isHidden = true
를 설정하면 해당 뷰는 화면에서 보이지 않는건 맞지만,
여전히 frame(크기와 위치)는 유지된다고 한다.
뷰의 크기나 위치는 0이라 하더라도 여전히 이벤트를 처리하려고 할 수 있다는데,
isHidden = true
일 때는 기본적으로 해당 뷰가 터치 이벤트를 받을 수 없도록 처리되는 것이고,
여기서 스크롤이 되지 않는 이유는 tableView
의 height = 0
으로 설정돼서
스크롤 콘텐츠 자체가 화면에 표시되지 않거나 이벤트가 해당 뷰로 제대로 전달되지 않아서일 가능성이 크다고 한다.
height = 0
으로 설정된 뷰는 화면상에서 실제로 존재하지 않는 것과 동일한 효과를 낳지만,
나는 isHidden = true
와 함께 사용했기 때문에 뷰는 화면에 그려지지 않았다.
그래서 여전히 메모리 상에 존재하고 있던 것이고,
그 크기(높이 0)가 스크롤 이벤트에 영향을 줄 수 있던 것.
tableView
의 높이가 0
이면 스크롤 가능한 콘텐츠가 존재하지 않아서 스크롤 동작이 실제로 표시되는 콘텐츠에 대해서만 동작하게 되는데,
스크롤 뷰는 contentSize
에 따라 동작하기 때문에 height = 0
일 때는 스크롤이 동작할 콘텐츠가 없어서 제대로 스크롤되지 않을 수 있다는 설명이 될 수 있다.
isHidden = true
를 사용하면 뷰는 화면에 보이지 않으며 터치 이벤트를 차단하게 되지만,
해당 뷰의 크기와 위치는 여전히 존재해서 그 영역을 차지하게 되기 때문에
이벤트가 발생할 수 있는 영역이 그대로 남아 있을 수 있다는 것.
그럼에도 불구하고 터치 이벤트는 차단되니까 사용자는 해당 뷰를 터치할 수 없고, 이벤트는 다른 뷰로 전달되는 것이다.
hitTest를 사용해서 터치 이벤트가 테이블 뷰로 전파되지 않도록 설정한 이유는,
tableView.isHidden = true
와 height = 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
}
point는 터치된 지점의 좌표인데 그 지점은 tableView.frame
테이블 뷰의 프레임이다.
그래서 frame.contains(point)
는 터치된 지점이 tableView의 프레임 내부에 있는지 확인하는데,
결과가 true
인 경우는 터치가 tableView 내부에서 발생한 것이라 볼 수 있고
결과가 false
인 경우는 터치가 tableView 외부에서 발생한 것이라 볼 수 있다.
이 조건을 걸은 건,
터치가 tableView 외부에서 발생했을 때만 아래 코드를 실행하도록 하는데,
else
부분에서 super.hitTest(point, with: event)
를 호출해 부모 뷰의 기본 동작을 수행하도록 한 것이다.
기본적으로 부모 뷰의 hitTest
메서드를 호출해 이벤트 처리를 진행하게 되고,
이 코드가 실행되면 터치 이벤트는 tableView가 처리할 수 있게 된다.
nil
을 반환하면 터치 이벤트가 처리되지 않고, 이벤트는 더 이상 전달되지 않는다.
이 경우는 tableView 외부에 터치가 발생하면 아무런 반응이 없게 된다.
테이블뷰 영역에서도 스크롤이 원활하게 작동하는 걸 볼 수 있다.
라며 gif 를 똑같이 올리는데 자꾸 이미지 업로드가 실패한다...
나중에 업로드 꼭 해야지.
아무튼 스크롤이 테이블뷰 영역 안에서도 잘 되는걸 확인했다.
테이블뷰 영역에 스크롤이 되지 않는 버그를 팀원이 발견하고 해결방법을 알아보았지만
도무지 되지 않아 튜터님에게 질문하여 알게 된 메서드였다.
사실 그 당시 나는 이해가 잘 가지 않아서 아 hitTest라는 메서드가 그냥 최상단의 뷰를 터치해주는 메서드라고 간단하게 생각하고 넘어갔는데,
이 부분이 우리 아이맥도날드에서 나름 중요하게 쓰인 메서드라고 생각이 들어 다시 공부하고 싶었다.
오늘 hitTest에 대해 공부하면서 터치 이벤트가 어떻게 뷰 계층에서 처리되는지에 대해 중요한 개념들을 이해할 수 있었다.
hitTest는 화면에서 터치가 발생했을 때 그 터치가 어떤 뷰에 전달될지 결정하는 메서드인 것이고,
터치 위치를 기준으로 적합한 뷰를 찾아 반환한다는 점을 알게 됐다.
isHidden = true와 같은 속성이 뷰에 적용되었을 때,
해당 뷰가 보이지 않지만 여전히 크기와 위치를 차지하고 있기 때문에
터치 이벤트가 그 뷰에 영향을 미칠 수 있다는 점도 이해할 수 있었고,
이때 hitTest를 오버라이드하면 터치 이벤트를 명시적으로 처리할 수 있다는 점이 정말 유용하다 생각했다.
전반적으로 hitTest는 화면 상에서 터치가 발생할 때 그 터치를 받을 뷰를 결정하는 중요한 역할을 한다는 걸 알았으니 ..
이건 앞으로의 과제를 수행하면서도 꼭 기억해야할 메서드라는 것을 느꼈다.
오,, 이번 프로젝트에서 진짜 많은 메서드를 사용해보셨네요
미리 테이블 뷰도 공부해두고 멋지네