[iOS] HitTest
- -
Responder Chain에서 나온 hitTest
터치 이벤트에 반응한 View가 어떤것인지 알아보기 위해 필요한 것이 hitTest
HitTest
터치 이벤트가 발생한 포인트에 있는 view중 view hierarchy에서 가장 멀리 있는 하위 view
즉 최상단의 view를 반환
여기서 말하는 최상단의 View는 view hierarchy의 최상단 객체가 아니라
사용자가 보았을 때 가장 상위의 View를 말함
아래 사진에서 검은색 화살표 부분을 터치했을 때를 예시로 들면 ViewB를 말함
![]() |
![]() |
hitTest가 필요한 이유
hitTest를 사용하여 터치 이벤트를 받을 view를 정할 수 있다.
ViewB의 hitTest를 사용하면 위 그림과 같이 ViewB를 터치했을 때 ViewB를 터치한 것이 아니라
터치 이벤트가 발생한 포인트를 포함하고 있는 ViewA나 ViewA2를 터치한 것처럼 만들 수 있다.
hitTest는 어떻게 사용하나 ?
class ViewB: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitview = super.hitTest(point, with: event)
return hitview == self ? nil : hitview
}
}
hitTest에서 nil을 반환하면 해당 view에서 이벤트를 처리하지 않고 이벤트를 처리할 다른 view를 찾으러 간다.
여기선 viewA2가 터치 이벤트를 처리하게 된다.
hitTest의 동작 원리
가장 최상단의 뷰 즉 view hierarchy에서의 최하단의 view를 찾기위해 hitTest는
reverse pre-order DFS를 사용하여 view hierarchy를 탐색한다
위의 view가 추가된 순서는 A → B → C 순이다.
- mainView에 ViewA 추가
- ViewA.1 추가
- ViewA.2 추가
- mainView에 ViewB 추가
- ViewB.1 추가
- ViewB.2 추가
- mainView에 ViewB 추가
- ViewB.1 추가
- ViewB.2 추가
여기서 ViewB.1을 터치하게 되면 hitTest는 reverse pre-order DFS탐색을 통해
window → mainView → ViewC → ViewB → ViewB.2 → ViewB.1 을 탐색하게 된다.
ViewC에서 subView인 ViewC.1, ViewC.2를 탐색하지 않는 이유는 그들의 상위 view인 ViewC가
터치가 일어난 point를 포함하고 있지 않기 때문에 subView들도 해당 point를 포함하고 있지 않을 것이기에
더 이상 아래(ViewC.1, ViewC.2)를 탐색하지 않고 옆(ViewB, ViewA)을 탐색하게 된다.
Ex
import UIKit
extension UIView {
var name: String { String(describing: type(of: self)) }
}
extension UIWindow {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print(name, #function)
let hitView = super.hitTest(point, with: event)
if hitView == self {
print(name, "hit")
}
return hitView
}
}
class View: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print(name, #function)
let hitView = super.hitTest(point, with: event)
if hitView == self {
print(name, "hit")
}
return hitView
}
}
final class MainView: View { }
final class ViewA: View {}
final class ViewA1: View {}
final class ViewA2: View {}
final class ViewB: View {}
final class ViewB1: View {}
final class ViewB2: View {}
final class ViewC: View {}
final class ViewC1: View {}
final class ViewC2: View {}
A1 터치
A2 터치
B1 터치
B2 터치
C1 터치
C2 터치
MainView 터치
여기서 hitTest가 동일하게 2번 호출되는데 터치한 뷰에 따라 아래와 같이 호출되는 것을 확인할 수 있다.
- A1
- C → B → A2
- A2
- C → B → A1
- B1
- C → B2 → A
- B2
- C → B1 → A
- C1
- C2 → B → A
- C2
- C1 → B → A
- MainView
- C -> B -> A
공식문서와 구글링을 통해 찾아보니 hitTest는 내부에서 point를 호출하여 계속해서 view들을 탐색하기 때문에 hitTest가 여러번 호출되는 것은 오류가 아니다라는 것이다.
그렇기에 hitTest와 point를 출력해보면 여러번 호출되는 것이다.
class View: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print(name, #function)
return super.point(inside: point, with: event)
}
}
hitTest가 여러번 호출되어도 터치된 view를 찾기만 하면 되기 때문에 터치된 view를 찾고 난 뒤
responder에게 터치된 view만 제대로 전달해주면 된다는 것
실제로 responder를 출력해보면 문제가 없다.
extension UIView {
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print(name, #function)
}
}
hitTest를 오버라이드 하여 하위 view의 hit을 가로챌 수 있다.
final class ViewB: View {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print()
print("Override HitTest", name)
print()
return self
}
}
주의할 점
// hitTest 내부
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 {
return nil
}
if self.point(inside: point, with: event) {
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
return self
}
return nil
}
- isUserInteractionEnabled = false
- isHidden = true
- alpha ≤ 0.01
위 경우에 대해서는 hitTest를 무시한다.
https://github.com/junbok97/iOS-Study/tree/main/%EB%B8%94%EB%A1%9C%EA%B7%B8/HitTest
iOS-Study/블로그/HitTest at main · junbok97/iOS-Study
Contribute to junbok97/iOS-Study development by creating an account on GitHub.
github.com
참고
hitTest(_:with:) | Apple Developer Documentation
hitTest(_:with:) | Apple Developer Documentation
Returns the farthest descendant in the view hierarchy of the current view, including itself, that contains the specified point.
developer.apple.com
iOS ) hitTest
안녕하세요 :) Zedd입니다. 라이브러리를 사용하면서 소스 보면 가아끔 hitTest가 있었는데, 뭐지?하고 그냥 지나쳤던 기억이...오늘 제대로 공부해볼려고 해용이를 위해서..UIResponder를 썼었죠.. hitTe
zeddios.tistory.com
View 계층 탐색 알고리즘과 Hit-Testing in iOS
View 계층 탐색 알고리즘과 Hit-Testing in iOS
explain how to find UIView object to handle events with reverse pre-order depth-first traverse algorithm and Hit-testing
lena-chamna.netlify.app
[iOS][Swift] Responder Chain, hitTest, point (hitTest, point의 호출 로직
[iOS][Swift] Responder Chain, hitTest, point (hitTest, point의 호출 로직)
UIResponder에 대해 자료를 알아보던 중 Responder Chain에 대한 과정을 직접 확인하고 싶어 hitTest와 point를 활용하여 디버깅을 하던 중 흥미로운 점을 발견하였습니다 그전에 Responder Chain이란? 대부분의
itllbegone.tistory.com
'iOS > iOS' 카테고리의 다른 글
[iOS] App Sandbox (1) | 2024.07.01 |
---|---|
[iOS] UIResponder, Responder Chain, First Responder (0) | 2024.06.09 |
[iOS] 모듈화 Library, Framework, Package, (0) | 2024.05.09 |
[iOS] 하위 뷰의 frame이 잡히지 않을 때 (1) | 2023.12.28 |
[iOS] UITableViewCell 또는 UICollectionViewCell 에서 Bind할 때 주의점 (0) | 2023.12.28 |
소중한 공감 감사합니다