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
}