앞 글에서 간단하게 소개했지만 Responder Chain은 간단하게 알고 넘어갈 만한 것이 아니다. 어떤 식으로 이벤트를 받아야 하는 오소를 감지하는지, 그리고 어떻게 이벤트를 넘기는지에 관한 내용을 이해해야 사용자와의 제대로된 interaction이 있는 앱을 만들 수 있을 것이다. 이번에는 Hit Test와 Responder chain에 대해서 제대로 알아보도록 하자. 그럼 시작하자.
가장 상위 (View 계층 구조에서 이벤트 발생시, 가장 위에 있는 UI 요소) 요소로 이벤트가 전달되고, 만약 해당 이벤트를 받지 못한다면, 그보다 상위에 있는
UIResponder
객체로 이벤트가 전달되는 방식
사용자는 이벤트를 발생시킨다. 그 이벤트의 종류는 다양하겠으나 일단 터치를 생각해보자. View는 계층을 이루면서 구현되게 되는데, 특정 요소가 터치되었다는 것을 어떻게 알 수 있을까? 그리고 특정 요소가 터치되지 않길 바란다면, 사용자의 터치 자체는 바로 무시되는 것일까?
이러한 의문에 대한 답이 Responder Chain이다. 먼저 Hit test를 통해 view 계층에서 가장 상위에 있는 view를 알아낸다. 그리고 이 View로 Event가 전달되고, 이를 처리하는 로직이 없는 경우, 해당 View의 상위(superView) view로 이벤트가 전달된다. 최종적으로 UIApplication에서 이벤트가 전달되고, 받지 못하는 경우 소멸된다. 여기서 View라 통칭했으나, 실질적으로 UIResponder
객체를 상속받은 객체(예: UIView
, UIViewController
, UIApplication
)를 말한다.
이벤트를 처리하는 객체는 UIResponder를 상속 받아야 한다. 대부분의 우리가 아는 View 요소들이 해당 클래스를 상속해서 만들어져 있다.
Event는 디바이스와 사용자가 서로 소통하기 위한 일종의 인터페이스라 생각할 수 있을 것이다. 터치, 리모컨 버튼 누름, 모션 등등 다양한 이벤트가 발생할 수 있고 디바이스는 이를 수신한다. 터치를 우리는 아마 가장 많이 다루게 될 것이다.
이 부분에서 알아두어야 하는 것은, 이벤트 객체가 같은 이벤트에 대해서 재사용된다는 것이다. 즉, 어플리케이션이 실행되고, 특정 이벤트(터치)가 처음으로 발생되면, 이 이벤트 객체가 생성되고 다음 이벤트에서도 재사용된다. 생성된 이벤트가 터치라면, UITouch 객체를 포함한다.
터치 이벤트 발생시, 이벤트 객체와 동시에 발생한 터치에 관한 정보를 담는 UITouch 객체가 생성된다. 내부에 들어있는 정보는 아래와 같다.
func location(in: UIView?)
method)addTarget(_:action:for:)
method로 이벤트와 액션을 연결한다. UIControl은 UIResponder 메소드보다 상위(고수준) 메소드를 제공한다. 즉, 이벤트에 대한 분석이 모두 끝난 상태로 적용이 가능하다는 것이다.
예를 들어, UIButton의 .touchUpInside
의 경우 버튼 boundary내에서 터치가 시작되고 끝났을 때를 의미하는 UIControl.Event
객체이다. 해당 메서드를 사용하지 않고 UIResponder가 제공하는 method (touchesBegan
, touchesEnded
override)로 구현한다면 몇단계나 추가되어야 할까? 터치 시작, 끝에 해당하는 메서드를 override해야 할 것이고, target-action으로 연결도 해주어야 할 것이다. 상당히 귀찮다.
그럼 터치가 어디서 발생했는지는 어떻게 알 수 있을까? 즉, 터치가 발생한 시점에, 어떻게 내가 누른 가장 상위 객체(상단은 view 계층에서 top을 말함)를 판단할 수 있을까? 여기서 이벤트에 반응한 가장 상위 객체를 first Responder
라 한다.
그림이 어려울 수 있는데, 쉽게 설명해보겠다. 위의 그림을 보면 사용자가 특정 View 요소를 선택하고, 이동한 뒤, 떼는 작업을 하고 있다. 이 각각의 단계에서 아래에는 어떤 동작들이 이루어지는지 적혀있다. Hit test는 이 시점에서, 가장 처음 실행된다. 즉, 사용자가 터치를 실행하는 순간, Hit test를 통해 first responder가 어떤 녀석인지 확인한다.
그리고 drag를 하면서 일정 시간을 기준으로 사용자 이벤트가 계속 발생한다. 그래서 화살표가 3개이다. 이 이벤트들은 첫번째 hit test의 결과인 first responder에게 전달된다.
마지막으로 터치를 뗀 경우, 뗀 event가 first responder로 전달된다. 어떄용 쉽죠?
func hitTest(point: CGPoint) -> UIResponder? {
guard isPoint(inside: point) else { return nil }
guard subViews.count > 0 else { return self }
let pointDiff = CGPoint(x: point.x = origin.x, y: point.y - origin.y)
for subView in subViews.reversed() {
let hitView = subView.hitTest(point: pointDiff)
if hitView != nil {
return hitView
}
}
return self
}
해당 코드는 간략하게 동작을 나타내 본 것이다. 참고로 subview들은 하위에 있을 수록 array에 아래에 있다. (stack 구조이다.) 알고리즘에서 생각해야 하는 부분은 다음과 같다.
UIApplication
에서 이벤트를 받아 하위로 전달한다.UIApplication
은 first responder를 사용하지 않는다.이런 알고리즘을 Reverse Pre-order depth-first traversal algorithm
이라 한다고 한다. 그런데 사실 DFS와 크게 다른점이 없다고 생각해서 깊게 작성하지는 않았다. 또 몇가지 더 고려하는 사항이 있긴한데, 중요하지 않아서 제거했다. 고려 사항은 다음과 같다.