[ios] Responder Chain and Touch Event
[ios] Responder Chain and Touch Event
Overview
앱은 응답자 객체를 이용해 이벤트를 받고 처리합니다. 응답자 객체는 UIResponder
클래스의 객체로 서브클래스로는 UIView
, UIViewController
, UIApplication
등이 있습니다. 응답자 객체는 이벤트 데이터를 받고 반드시 이를 직접 처리하거나 다른 응답자 객체에게 전달해야 합니다. 이벤트가 발생하게 되면 앱은 해당 이벤트를 처리할 수 있는 가장 적절한 응답자 객체에게 이벤트 데이터를 전달하고 이를 first responder라 합니다.
first responder가 이벤트를 반드시 처리하라는 법은 없죠! 이렇게 처리되지 않은 이벤트들은 first responder로부터 시작하는 Repsonder Chain을 따라 이벤트를 처리하는 객체를 찾아 거슬러 올라갑니다. 이런 Responder Chain은 앱의 구조에 따라 동적으로 형성됩니다.
다음의 다이어그램을 예를 들어 살펴보도록 하겠습니다.
만일 UITextField
에 이벤트가 들어왔다고 가정했을 때 UITextField
가 해당 이벤트를 처리하지 않는다면 그 이벤트 객체는 부모 뷰인 UIView
에게 넘어가고 역시 처리되지 않으면 UIViewController
, UIWindow
순으로 거슬러 올라가며 자신을 처리해줄 응답자 객체를 찾습니다. 하지만 끝까지 처리되지 않은 이벤트들은 버려지게 됩니다.
그렇다면 이러한 이벤트에는 어떤 것들이 있을까요? 이벤트 타입과 이벤트 타입 별 first responder를 살펴보겠습니다.
Event Type
Event Type | First Responder |
---|---|
Touch Events | Touch가 발생한 뷰 |
Press Events | Press가 발생한 뷰 |
Shake-motion Events | 사용자 혹은 UIKit이 지정한 객체 |
Remote-control Events | 사용자 혹은 UIKit이 지정한 객체 |
Editing Menu Messages | 사용자 혹은 UIKit이 지정한 객체 |
Editing Menu는 텍스트를 길게 눌렀을 때 나타나는 편집 메뉴를 말합니다. (Select, Select all, Copy 등등)
여러 이벤트 타입 중 오늘은 터치 이벤트에 집중해서 포스팅해보도록 하겠습니다.
Touch Events
터치 이벤트가 발생하는 순간부터 이벤트가 응답자 객체에게 전달되는 과정을 먼저 살펴보도록 하겠습니다.
Hit Testing
먼저 터치 이벤트가 발생하면 해당 터치가 어느 뷰에서 발생했는지를 아는 것이 중요합니다. 기본적으로 터치가 발생한 뷰가 해당 이벤트에 대한 first responder가 되기 때문에 터치 이벤트 객체를 해당 뷰에 전달해야하기 때문입니다.
iOS에서는 Hit Testing을 통해 터치가 발생한 뷰를 찾아낼 수 있고 해당 뷰에 이벤트 객체를 전달합니다. Hit Testing에 관해서는 제가 작성한 글을 참고해주시기 바랍니다.
이렇게 이벤트가 전달되는 과정을 자세히 살펴보기 위해서는 몇 가지 클래스들에 대해 살펴보아야 합니다.
UIResponder
UIEvent
UITouch
UIResponder
위에서 언급했듯이 이벤트를 처리하는 객체가 바로 UIResponder
를 상속받은 클래스의 객체들입니다. UIView
, UIViewController
그리고 UIWindow
등이 있습니다. 전달되는 이벤트 종류에 따라 해당 클래스들의 UIResponder
메소드들을 재정의해주면 됩니다.
이벤트 타입 별 메소드들이 존재하지만 터치 이벤트를 핸들링하는 메소드들만 간략하게 나열해보겠습니다.
Responding to Touch Events
func touchesBegan(Set<UITouch>, with: UIEvent?)
func touchesMoved(Set<UITouch>, with: UIEvent?)
func touchesEnded(Set<UITouch>, with: UIEvent?)
func touchesCancelled(Set<UITouch>, with: UIEvent?)
func touchesEstimatedPropertiesUpdated(Set<UITouch>
다른 이벤트들에 대한 메소드는 공식 문서를 참고해주세요.
또한 응답자 객체는 이벤트 객체를 다룰 뿐만 아니라 inputView
를 통해 입력 값도 받을 수 있습니다. 대표적인 예가 바로 UITextField
로 UITextField
를 터치하면 시스템 키보드가 올라오는 것을 확인하실 수 있습니다.
UIEvent
이벤트 객체는 타입별로 이벤트가 발생했을 때 생성되고 응답자 객체에 전달됩니다. 이벤트 객체에서 중요한 것은 같은 이벤트에 대한 객체는 재사용된다는 것입니다. 즉 어플리케이션이 실행되고 터치 이벤트가 처음으로 발생되면 터치 이벤트를 담는 이벤트 객체가 생성되고 이 객체는 다음 터치 이벤트에서도 재사용됩니다. 이는 연속적이지 않아도 해당됩니다.
저는 객체가 재사용되는지를 확인하기 위해 객체의 고유값을 확인할 수 있는 ObjectIdentifier
를 사용하여 비교하였습니다. ObjectIdentifier
의 예시는 다음과 같습니다.
그리고 다음 이를 활용하여 이벤트 객체에 적용시켜 출력한 결과입니다.
다른 이벤트와 다르게 발생한 이벤트가 터치 이벤트라면 이벤트 객체는 UITouch
객체를 포함하게 됩니다. 그리고 이벤트 객체의 다양한 프로퍼티로 해당 터치 객체들에 접근이 가능합니다. 이런 터치 객체들은 하나 혹은 그 이상이 될 수 있습니다.
이를 표현한 다이어그램은 다음과 같습니다.
이젠 UITouch
를 좀 더 살펴보도록 하겠습니다.
UITouch
터치 이벤트가 발생하면 이벤트 객체와 동시에 발생한 터치에 관한 정보를 담는 UITouch
객체가 생성됩니다. 터치에 관한 정보는 다음과 같습니다.
터치가 발생한 뷰나 윈도우 (
view
,window
프로퍼티)터치가 발생한 뷰/윈도우에서의 좌표 (
func location(in: UIView?)
메소드)터치의 반지름 (
majorRadius
프로퍼티)터치의 강도 (
force
프로퍼티)터치 횟수 (
tapCount
프로퍼티)- 같은 좌표에 대한 터치 발생 시 증가
터치된 순간의 시간 (
timeStamp
프로퍼티)
기타 다른 정보들에 대해서는 공식 문서를 참고해주세요
터치 객체 또한 재사용됩니다. 손가락이 화면에 닿고 때어질 때까지 동일한 터치 객체를 사용합니다. 즉 객체는 동일하고 내부 프로퍼티만 갱신되는 것입니다.
콘솔에 출력된 결과물을 살펴보면 손가락이 화면에 닿고 움직이는 동안 동일한 객체를 사용하는 것을 확인할 수 있습니다. 이렇게 동일한 객체를 사용하지만 내부의 위치 프로퍼티 등은 갱신되어집니다. 그렇다면 손가락이 때어졌을 때 이 터치 객체는 release될까요? 다음 출력 화면을 살펴보도록 하겠습니다.
동일한 지점에 대해 계속해서 터치 이벤트가 발생하면 tapCount
프로퍼티가 증가하시는 걸 확인하실 수 있습니다. 하지만 13번의 터치에 13개의 터치 객체가 생성되는 것이 아닌 4개의 객체만이 사용되었고 손가락을 때어도 release되지 않고 이를 재사용하고 있다는 것을 알 수 있습니다.
이러한 현상은 같은 좌표에 대한 터치가 아니어도 동일하게 발생합니다. 이렇게 객체가 재사용되기 때문에 특정 이벤트나 터치 객체를 그 상태(터치의 경우 탭 횟수나 탭 좌표 등) 그대로 추후에 사용하고 싶다면 단순 참조가 아닌 객체를 복사하여 사용해야 합니다.
저는 여기서 하나의 의문점이 생겼습니다. UIResponder
객체 메소드인 touchesBegan
, touchesMoved
그리고 touchesEnded
와 UIControl
의 Target-Action과의 관계가 어떻게 되는지가 궁금했습니다.
UIControl
UIControl
에 대한 내용은 제가 작성한 포스팅을 참고해주세요. UIControl
은 addTarget(_:action:for:)
매소드로 이벤트와 액션을 연결시켜 처리합니다. UIControl
은 이벤트 처리에 있어서 UIResponder
메소드 보다 상위 수준의 메소드를 제공합니다. 그 말인즉슨 UIControl
이 이벤트를 처리할 때는 이미 이벤트에 대한 분석이 모두 끝난 상태라는 것입니다.
UIButton
의 .touchUpInside
는 버튼을 버튼 boundary 내부에서 터치가 시작되고 끝났을 때를 말하는 UIControl.Event
입니다. 하지만 만약에 UIButton
클래스 안에서 touchesBegan
과 touchesEnded
메소드를 구현하면 어떻게 될까요?
UIRESPONDER : Button Touch Began
TARGET-ACTION : Button Did Tapped
UIRESPONDER : Button Touch Ended
버튼에 대한 터치가 들어오고 touchesBegan
메소드가 호출됩니다. 그리고 터치를 한 손가락이 버튼에 boundary안에서 때어지면 그 순간 버튼 안에서 터치가 일어나고 끝났다는 결론을 내립니다. 그리고 이런 분석 결과는 .touchUpInside
과 일치하기 때문에 Target-Action에 의한 메소드가 호출되는 것입니다.
이러한 분석 결과는 마지막 touchesEnded
가 호출되고 나서 결과가 나오기 때문에 super.touchesEnded(touches, with: event)
다음에 위치한 print("UIRESPONDER : Button Touch Ended")
문에 의한 출력문은 Target-Action 메소드 출력문 다음에 나오는 것입니다.
만일 터치의 시작은 버튼안에서 시작했지만 손가락을 끌어 버튼의 바깥에서 때어냈다면 결과는 다음과 같습니다.
UIRESPONDER : Button Touch Began
UIRESPONDER : Button Touch Ended
위의 touchesBegan
과 touchesEnded
메소드를 주석처리하고 sendAction
메소드를 재정의해보도록 하겠습니다.
즉 이벤트의 분석이 끝났다는 것은 UIControl.Event
에 해당하는 .touchUpInside
는 "터치 이벤트의 시작 좌표와 끝났을 때의 좌표가 모두 터치가 발생한 뷰의 안에 해당하고 터치가 끝난 시점"을 의미하고 이 모든 것에 대한 이벤트의 분석을 의미합니다.
그러므로 위의 sendAction
메소드로 전달되는 이벤트 객체 안의 터치 객체는 끝난 시점을 나타냅니다.
Target-Action 메소드의 출력문이 먼저 나온 이유는
super.sendAction(action, to: target, for: event)
가 먼저 호출되었기 때문입니다.switch
문과의 순서를 바꾸면 출력 순서 역시 바뀝니다.
마무리
오늘은 이렇게 터치 이벤트의 발생부터 이를 처리하는 단계까지 전반적으로 알아보는 시간이었습니다. 이렇게 이벤트가 처리되고 처리하는 과정과 그 주체를 알게 되면 보다 이벤트를 디테일하게 처리할 수 있을 것 같습니다. 감사합니다.
참고자료