Hit Test, Responder Chain

최완식·2022년 2월 13일
0

UIKit

목록 보기
4/33
post-thumbnail
post-custom-banner

앞 글에서 간단하게 소개했지만 Responder Chain은 간단하게 알고 넘어갈 만한 것이 아니다. 어떤 식으로 이벤트를 받아야 하는 오소를 감지하는지, 그리고 어떻게 이벤트를 넘기는지에 관한 내용을 이해해야 사용자와의 제대로된 interaction이 있는 앱을 만들 수 있을 것이다. 이번에는 Hit Test와 Responder chain에 대해서 제대로 알아보도록 하자. 그럼 시작하자.

Responder Chain

가장 상위 (View 계층 구조에서 이벤트 발생시, 가장 위에 있는 UI 요소) 요소로 이벤트가 전달되고, 만약 해당 이벤트를 받지 못한다면, 그보다 상위에 있는 UIResponder 객체로 이벤트가 전달되는 방식

사용자는 이벤트를 발생시킨다. 그 이벤트의 종류는 다양하겠으나 일단 터치를 생각해보자. View는 계층을 이루면서 구현되게 되는데, 특정 요소가 터치되었다는 것을 어떻게 알 수 있을까? 그리고 특정 요소가 터치되지 않길 바란다면, 사용자의 터치 자체는 바로 무시되는 것일까?

이러한 의문에 대한 답이 Responder Chain이다. 먼저 Hit test를 통해 view 계층에서 가장 상위에 있는 view를 알아낸다. 그리고 이 View로 Event가 전달되고, 이를 처리하는 로직이 없는 경우, 해당 View의 상위(superView) view로 이벤트가 전달된다. 최종적으로 UIApplication에서 이벤트가 전달되고, 받지 못하는 경우 소멸된다. 여기서 View라 통칭했으나, 실질적으로 UIResponder 객체를 상속받은 객체(예: UIView, UIViewController, UIApplication)를 말한다.

알아두어야 할 용어

UIResponder

이벤트를 처리하는 객체는 UIResponder를 상속 받아야 한다. 대부분의 우리가 아는 View 요소들이 해당 클래스를 상속해서 만들어져 있다.

UIEvent

Event는 디바이스와 사용자가 서로 소통하기 위한 일종의 인터페이스라 생각할 수 있을 것이다. 터치, 리모컨 버튼 누름, 모션 등등 다양한 이벤트가 발생할 수 있고 디바이스는 이를 수신한다. 터치를 우리는 아마 가장 많이 다루게 될 것이다.

이 부분에서 알아두어야 하는 것은, 이벤트 객체가 같은 이벤트에 대해서 재사용된다는 것이다. 즉, 어플리케이션이 실행되고, 특정 이벤트(터치)가 처음으로 발생되면, 이 이벤트 객체가 생성되고 다음 이벤트에서도 재사용된다. 생성된 이벤트가 터치라면, UITouch 객체를 포함한다.

UITouch

터치 이벤트 발생시, 이벤트 객체와 동시에 발생한 터치에 관한 정보를 담는 UITouch 객체가 생성된다. 내부에 들어있는 정보는 아래와 같다.

  • 터치가 발생한 view, or window property
  • 터치가 발생한 view/window에서의 좌표 (func location(in: UIView?) method)
  • 터치의 반지름
  • 터치의 강도
  • 터치 횟수
  • 같은 좌표에 대한 터치 발생시 증가함
  • 처치된 순간의 시간

UIControl

addTarget(_:action:for:) method로 이벤트와 액션을 연결한다. UIControl은 UIResponder 메소드보다 상위(고수준) 메소드를 제공한다. 즉, 이벤트에 대한 분석이 모두 끝난 상태로 적용이 가능하다는 것이다.

예를 들어, UIButton의 .touchUpInside의 경우 버튼 boundary내에서 터치가 시작되고 끝났을 때를 의미하는 UIControl.Event 객체이다. 해당 메서드를 사용하지 않고 UIResponder가 제공하는 method (touchesBegan, touchesEnded override)로 구현한다면 몇단계나 추가되어야 할까? 터치 시작, 끝에 해당하는 메서드를 override해야 할 것이고, target-action으로 연결도 해주어야 할 것이다. 상당히 귀찮다.

Hit Testing

그럼 터치가 어디서 발생했는지는 어떻게 알 수 있을까? 즉, 터치가 발생한 시점에, 어떻게 내가 누른 가장 상위 객체(상단은 view 계층에서 top을 말함)를 판단할 수 있을까? 여기서 이벤트에 반응한 가장 상위 객체를 first Responder라 한다.

Hit Test 실행 시점

그림이 어려울 수 있는데, 쉽게 설명해보겠다. 위의 그림을 보면 사용자가 특정 View 요소를 선택하고, 이동한 뒤, 떼는 작업을 하고 있다. 이 각각의 단계에서 아래에는 어떤 동작들이 이루어지는지 적혀있다. Hit test는 이 시점에서, 가장 처음 실행된다. 즉, 사용자가 터치를 실행하는 순간, Hit test를 통해 first responder가 어떤 녀석인지 확인한다.

그리고 drag를 하면서 일정 시간을 기준으로 사용자 이벤트가 계속 발생한다. 그래서 화살표가 3개이다. 이 이벤트들은 첫번째 hit test의 결과인 first responder에게 전달된다.

마지막으로 터치를 뗀 경우, 뗀 event가 first responder로 전달된다. 어떄용 쉽죠?

Hit Test 작동 방식

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를 사용하지 않는다.
  • 내부에 들어있지 않으면 pass 한다.
  • 내부에 들어있는데 하위 view가 없다면 (내가 가장 상위 뷰) 자신을 반환한다.
  • 하위 view가 있는 경우, 내가 가장 위에 있지 않을수도 있으므로 하위 View로 Hit Test를 전달한다.

이런 알고리즘을 Reverse Pre-order depth-first traversal algorithm 이라 한다고 한다. 그런데 사실 DFS와 크게 다른점이 없다고 생각해서 깊게 작성하지는 않았다. 또 몇가지 더 고려하는 사항이 있긴한데, 중요하지 않아서 제거했다. 고려 사항은 다음과 같다.

  • View가 hidden인가?
  • UserInteraction이 Enable인가?
  • Alpha 수준이 일정 이상 (0.01 up) 인가?
  • 해당 이벤트가 포함되는가?

Reference

profile
Goal, Plan, Execute.
post-custom-banner

0개의 댓글