본문 바로가기
iOS/Swift

Swift) hitTest

by Jiseong 2022. 2. 20.

Responder Chain을 공부하던 도중.. 이 내용도 알아야할 것 같아서..

Hit Testing

hitTest라는 메서드가 있다.

 

이는 UIView의 인스턴스 메서드이다.

 

이 메서드를 통해 어떤 View가 이벤트를 처리할지 결정할 수 있다.

해당 메서드는 두개의 파라미터가 있는데, 말로 좀 풀어서 메서드를 설명해보자면..

  1. 특정 포인트에 속하는 뷰 중, 특정 이벤트를 처리할 수 있는 뷰가 있느냐? 
  2. 있으면 그 뷰의 하위계층(자식)은 처리할 수 있음?

여기서 나뉜다. 자식이 이벤트를 처리할 수 있는 경우와 없는 경우

  1. 아 처리할 수 있음?? 걔(자식)의 자식(자식의 자식)도 처리할 수 있음? 
  2. 아 안됨? 그럼 너(부모)가 리턴 고

설명이 잘 됐을랑가 모르겠지만, 이벤트를 받을 수 없는 뷰가 나올때까지 파고드는 것이다.

 

아래 그림을 보면 좀 이해가 될 것이다.

좀 헷갈릴 수 있는데, hitTest는 뷰의 최상위 계층부터 하위로 내려가며(순회) 테스트한다.

 

depth-first traversal in reverse pre-order방식으로 순회한단다.

 

이는 최상위 트리에서 하위 트리로 순회하는데, 하위 트리에 속한 자식 트리가 있을 경우 더 이상 자식이 없을 때까지 순회한다. 그래서 깊이 우선인 것임

요렇게 ㅎㅎ;

 

실제 파고드는 과정을 보자

이처럼 계층이 사용자 눈에 제일 먼저 보이는 뷰가 아닌 루트부터 파고드는 방식으로 이뤄져있다.

 

그래서 depth-first traversal in reverse pre-order이지 않을까 조심스레 예상해본다.

 

그리고 이 알고리즘을 사용함으로써 순회 반복 횟수를 줄이고 터치 포인트를 포함하는 가장 깊은 하위 view(first deepest descendant view)가 발견되면 검색 프로세스를 중지한다. 

 

이는 subview가 항상 superview 앞에 렌더링되고 sibling view(형제 뷰, 공통 계층)가 항상 하위 인덱스가 있는 sibling view 앞에 subview 배열에 렌더링 되기 때문이다.

 

위 문장이 이해가 좀 안될 수 있는데, 우리가 특정 뷰의 자식으로 여러 뷰를 추가할 때를 생각해보자

let jiseongView = UIView()
let jiseongSonView = UIView()
let jiseongdaughterView = UIView()
let jiseongDogView = UIView()

jiseongView.addSubview(jiseongSonView)
jiseongView.addSubview(jiseongdaughterView)
jiseongView.addSubview(jiseongDogView)


위 코드에서 jiseongView의 subView 배열은 이와 같다. 

 

[jiseongSonView, jiseongdaughterView, jiseongDogView]

jiseongView의 subView인 jiseongSonView, jiseongdaughterView, jiseongDogView가 겹쳐있다.

 

하지만 jiseongDogView의 subViewIndex는 jiseongSonView, jiseongdaughterView의 subViewIndex보다 높기떄문에 jiseongDogView가 가장앞에 렌더링되게 된다.

 

그럼 세 서브뷰가 겹치는 부분을 터치한다면, 가장 전면에 있는 뷰인 jiseongDogView가 반환이될 것이다.

 

이런 이유로 여러 개의 겹치는 뷰에 특정 포인트가 포함된 경우 맨 오른쪽 하위 트리에서 가장 깊은 뷰가 맨 앞 뷰가 된다.

“Visually, the content of a subview obscures all or part of the content of its parent view. Each superview stores its subviews in an ordered array and the order in that array also affects the visibility of each subview. If two sibling subviews overlap each other, the one that was added last (or was moved to the end of the subview array) appears on top of the other.”

 

다시 그림을 보면.. 사용자는 ViewB의 자식뷰인 ViewB.1을 터치했다.

 

루트(앱 시스템 관점으론 최상단)부터 파고들어 같은 공통 계층에 있는 ViewA, ViewB, ViewC중 ViewC부터 테스트를 진행한다.

 

터치가 이뤄진 포인트엔 ViewC가 위치하지 않으므로 hitTest가 진행될때 마다 실행되는 point메서드가 false를 리턴하며 다음 뷰를 테스트하게 된다.

 

ViewB를 테스트하며 ViewB는 터치가 이뤄진 포인트에 위치하므로 true를 리턴하여 해당 뷰의 자식 계층으로 가 테스트를 진행한다.

 

ViewB.2는 터치가 이뤄진 포인트에 위치하지 않기에 false를 뱉고, 공통 계층에 있는 ViewB.2를 테스트해보니 터치가 이뤄진 포인트에 위치하고, 이벤트도 받으므로 hitTest는 ViewB.2를 리턴하게 되는 것이다.

 

만일 특정 뷰가 이벤트를 받지 않게 하고싶다면? 방법이 있다.

  1. 뷰를 숨김 - 특정뷰.isHidden = true
  2. 뷰를 숨김(투명하게) 특정뷰.alpha <= 0.01
  3. 이벤트를 받지 못하게함 특정뷰.userInteractionEnabled = false
  4. hitTest override하여 로직 정의

새해 복 많이 받으세요.

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

Swift) COW(Copy On Write)  (0) 2022.02.20
Swift) self vs Self  (0) 2022.02.20
Swift) CoreData  (0) 2022.02.20
Swift) UIAlertController ActionSheet Programmatically  (0) 2022.02.20
Swift Performance (Allocation)  (0) 2022.02.20

댓글