UIResponder
Responder = 응답자
공식 문서에 따르면 '이벤트에 응답하고 처리하기 위한 추상 인터페이스'라고 설명되어 있다.
class UIResponder: NSObject
UIResponder의 인스턴스는 UIKit 앱 이벤트 처리 백본을 구성한다.
많은 UIWindow, UIApplication, UIViewController, 모든 UIView 객체들은 리스폰더이다.
터치, 모션, 원격제어, 프레스 이벤트 등이 있고, 특정 유형의 이벤트를 처리하려면 응답자가 해당 메서드를 재정의해야 한다.
- touchesBegan(_:with:)
- touchesMoved(_:with:)
- touchesEnded(_:with:)
- touchesCanceled(_:with:)
의 메소드들을 재정의하여 특정 이벤트를 핸들링할 수 있다.
Responder들은 UIEvent 객체를 처리할 수도 있고, input view를 통해 Custom Input을 받아들일 수도 있다.
시스템 키보드가 이 input view의 예시이다.
사용자가 UITextField/UITextView를 탭하면, 해당 View는 first Responder가 되고, input view(키보드)를 띄운다.
비슷하게 Custom input view를 띄울수도 있다.
방법 : custom input view를 responder의 inputView 프로퍼티에 할당
UIResponder Chain
이벤트 핸들링 말고도, UIKit Responder들은 처리되지 않은 이벤트를 상위 Responder에게 전달할 수 있다.
특정 정해진 Responder가 이벤트를 핸들링하지 않는 경우, 해당 Responder는 그 이벤트를 Responder Chain의 다음 객체에게 전달한다.
이벤트(메세지)는 처리될 때까지 계속 Chain을 따라 상위 Responder 객체들로 이동한다.
마지막까지 처리되지 않는 경우, 이벤트를 버린다.
Responder Chain 예시
- 만약, TextField가 event를 핸들링 하지 않으면, UIKit은 TextField의 부모인 UIView에게 event를 전달하고, 이어서 Window의 RootView에게 보내진다.
- RootView에서, Responder Chain은 event를 Window로 보내기 전에 방향을 바꿔 RootView를 소유하고 있는 ViewController로 보낸다.
- 만약, Window가 event를 처리하지 못하면, UIKit은 event를 UIApplication 객체로 전달한다. 그리고 AppDelegate가 UIResponder의 인스턴스이고 Responder Chain의 일부가 아니라면 event를 AppDelegate에게 전달한다.
Event의 First Responder 결정
많은 종류의 이벤트를 처음으로 받는 Responder 객체를 First Responder라고 한다.
First Responder는 대체로 앱이 이벤트를 핸들링하기 가장 적합하다고 간주하는 Responder이다.
앱이 이벤트를 받으면, UIKit이 해당 이벤트를 가장 적합한 Responder 객체인 First Responder에게 전달한다.
이벤트를 받기 위해서는, Responder 자신이 First Responder가 될 수 있음을 나타내야 한다.
UIResponder의 Subclass에서 canBecomeFirstResponder 프로퍼티를 오버라이드하여 true를 리턴해야 한다.
UIKit은 event 유형에 따라 첫 응답자로 개체를 지정한다.
컨트롤은 연관된 타겟 객체와 직접 액션 메세지를 이용해 소통한다.
액션 메세지는 이벤트는 아니지만 Responder Chain을 이용한다.
컨트롤의 타겟 객체가 nil일 경우, UIKit은 First Responder에서 시작하여 적절한 액션 메세지를 구현한 객체를 만날 때까지 Chain을 따라 전달한다.
View에 있는 GestureRecognizer의 경우 또한 터치 등을 인식하지 못하면 UIKit은 View로 터치를 보내고, View도 터치를 처리하지 않는다면, Chain을 따라 전달한다.
그렇다면 이벤트가 어디서 발생하였고, 어떻게 Responder를 결정할까?
Determining Which Responder Contained a Touch Event
- UIKit은 hit-test를 사용하여 터치 이벤트가 발생하는 위치를 결정한다.
- UIKit은 터치 위치를 View 계층에 있는 View 객체들의 Bounds와 비교한다.
- UIView의 hitTest(_:with:) 메소드는 특정 터치를 포함하는 가장 깊은 SubView를 찾기 위해 View 계층을 따라 이동하고, 이 가장 깊은 SubView가 터치 이벤트의 FirstResponder가 된다.
터치 위치가 뷰의 경계 밖이라면, hitTest(_:with:) 메서드는 해당 뷰와 그 뷰의 모든 서브뷰들을 무시합니다.
결과적으로, 뷰의 clipToBounds 프로퍼티가 false 라면, 그 뷰의 밖에 있는 서브뷰들은 터치를 포함하더라도 반환되지 않습니다.
터치 이벤트가 발생하면, UIKit은 UITouch 객체를 만들고 View와 연결한다.
터치 위치나 다른 파라미터들이 변경되면, UIKit은 같은 UITouch 객체를 업데이트한다.
변경되지 않는 유일한 프로퍼티는 view이다.
심지어 터치 위치가 원래 View의 바깥으로 이동했더라도, 터치의 view 프로퍼티는 변하지 않는다.
터치가 끝나면, UIKit은 UITouch 객체를 메모리에서 해제한다.
Altering the Responder Chain
Responder 객체의 next 프로퍼티를 오버라이드하여 Responder Chain을 변경할 수 있다.
(next를 그대로 사용하면, hitTest했을 때의 상위 View에게 전달되나?)
이 작업을 할 때, next Responder는 오버라이드한 프로퍼티에서 반환하는 객체입니다.
많은 UIKit 클래스들은 이미 이 프로퍼티를 오버라이드하여 특정 객체들을 반환하고 있다.
UIView
- 만약 해당 뷰가 ViewController의 RootView라면, next Responder는 UIViewController 객체이다.
- 아니라면, next Responder는 해당 뷰의 Super View이다.
UIViewController
- 만약 ViewController의 뷰가 Window의 RootView라면, next Responder는 Window 객체이다.
- 만약 ViewController가 다른 ViewController에 의해 present된 경우(모달로 띄워진 경우), next Responder는 presenting VC이다.
UIWindow
- 윈도우의 next Responder는 UIApplication 객체이다
UIApplication
- UIApplication 객체의 next Responder는 AppDelegate이다.
- 하지만 AppDelegate가 UIResponder의 인스턴스이면서 View, ViewController, 또는 앱 객체 자신이 아닐 때만 해당된다.
직접 해보면서 이해해보자
ViewController에는 View들과 Label, Button, TextField를 넣었다.
또한 RedView, BlueView, GreenView, YellowView 는 Custom class를 생성하여 똑같은 메소드를 오버라이드하였다.
GreenView Touch
아니 왜 2개나 올려? 첫번째 사진은 ViewController 만 있는 흰색 부분 클릭한거 아니야? -> 라고 생각할 수 있다..
실제로 둘다 GreenView Bounds 내에 부분을 터치한 것이다.
그렇다면 무엇이 다르길래 Log 또한 다른 것인가?
위에서 해당 문구를 다시 보자
터치 위치가 뷰의 경계 밖이라면, hitTest(_:with:) 메서드는 해당 뷰와 그 뷰의 모든 서브뷰들을 무시합니다.
결과적으로, 뷰의 clipToBounds 프로퍼티가 false 라면, 그 뷰의 밖에 있는 서브뷰들은 터치를 포함하더라도 반환되지 않습니다.
터치 위치가 뷰의 경계 밖이라면 해당 뷰와 그 뷰의 모든 서브뷰를 무시한다고 나와있다.
터치 위치가
흰색 점일 때, 이는 GreenView의 Bounds 내에는 존재하지만,
BlueView의 Bounds 내에는 존재하지 않는다.
따라서 BlueView와 BlueView의 모든 서브뷰(GreenView)를 무시한다.
그렇다면 2번째는 어딜 터치했을까? 당연히 GreenView의 Bounds 내이기도 하면서 BlueView BOunds의 내이다.
마찬가지로 Yellow도 같은 원리로 동작한다.
RedView Touch
아니 또 장난치네, Red 뒤에 Green이랑 Yellow View가 있는데 왜 쟤네는 안불려? 라고 할 수도 있지만
뷰 계층을 다시 보자
RedView와 BlueView는 ViewController의 RootView에 붙어있다.
이로써 터치했을 때, 위에서 말했던 것처럼 UI로 보이는 것이 아닌 뷰 계층을 따라서 전달된다는 것을 알 수 있다.
Label Touch
Label은 기본적으로 Interaction이 불가능하기 때문에 User Interaction을 true로 설정해줘야 한다.
Button Touch
버튼은 super.touchesBegan 메소드를 사용 시, Chaining이 되지 않는다.
next?.touchesBegan 메소드를 사용시 Chaining이 정삭적으로 동작한다.
정리
기본적으로 touches~~ 메소드들은 내부적으로 super.touches~~ 메소드가 작성되어 있다.
따라서, 내가 RedView에서 Touch를 했을 때, ViewController에서 어떠한 동작을 하고 싶다!
하면 touchesBegan 메소드를 오버라이드해서 원하는 조건이 맞으면 원하는 동작을 수행하면 된다.
동작을 구현할 위치(Responder)가 ViewController가 아닌 Window나 AppDelegate여도 된다.
'iOS' 카테고리의 다른 글
[iOS] UIWindowScene / UIWindow / UIView (0) | 2021.05.23 |
---|---|
[iOS] HitTest (0) | 2021.05.21 |
[iOS] State Preserving/Restoring ViewControlle (0) | 2021.05.20 |
[iOS] App Life Cycle (0) | 2021.05.18 |
[iOS] TabBar Slide Down/Up Animation (0) | 2021.03.25 |