
챌린지 때 간단하게만 보고 지나간 iOS에서 이벤트 처리하는 과정을 다시 한번 학습해보자.
처음은 각 과정에서 등장하는 구성요소의 개념부터 학습해보자.
1. UIEvent
앱에서 하나의 유저 인터렉션을 설명하는 객체

앱은 touch, motion, remote-control 과 같은 여러 타입의 이벤트를 받는다.
이러한 이벤트는 type 와 subtype의 프로퍼티를 통해서 타입을 결정할 수 있다.
유저 인터렉션이 type과 subTpye을 통해서 분류되는 것을 확인할 수 있다.
실제로 type에는 touches, motion , remoteControl, presses 등이 있으며,
subType에는 motion, remoteControl의 이벤트를 조금 더 세분화 해서 분류한 것을 확인할 수 있다.
여러개의 터치를 분석하거나, 유저 인터렉션에 대해서 타입을 분류해서 전달되는 객체라고 정리할 수 있다.
요약하자면 Event가 발생하면 바로 UIEvent 인스턴스가 생성되고 다음과 같은 정보를 알 수 있다.

eventType은 0(TouchEvent)이고, TouchEvent는 별도의 subtype이 없으므로 none(0)
또한 allTouches 프로퍼티를 통해 발생한 모든 UITouch 인스턴스의 정보를 알 수 있다.
2. UITouch

가장 간단한 이벤트인 터치 이벤트 객체를 먼저 살펴보자.
일단 터치를 하면 iOS는 UITouch라는 인스턴스를 만들어낸 후, 특정 View에 종속되며
Touch가 종료되는 시점에서 사라진다. 여기서 특정 View는 아마 터치 이벤트를 받은 View로 추측된다.
UITouch 인스턴스가 제공해주는 정보를 한번 살펴보자.

터치 이벤트가 발생한 뷰, 위치, 터치 타입, 강도등 생각보다 많은 정보를 제공해준다.
아래 표를 보면 각 이벤트에 해당되는 first Responder를 정리해 놓은 자료가 있다.
first Responder는 아래에서 설명할테니 어떤 느낌인지만 보자.
이벤트 종류 | First Responder |
Touch events | 터치가 발생한 뷰 |
Press events | 포커싱된 객체 |
Shake-motion events | 개발자 또는 UIkit에서 지정한 객체 |
Remote-control events | 개발자 또는 UIkit에서 지정한 객체 |
Editing menu messages | 개발자 또는 UIkit에서 지정한 객체 |
3. Hit Test
Hit Test란 Touch 이벤트가 발생한 최상단 뷰를 찾는 과정이다.
왜 필요할까?
먼저 배치는 다음과 같다
- veiw1(red)
- view2(blue)
- view3(yellow)

살펴볼 함수는 2개가 있다.
1. point 함수
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool
이 함수의 역할은 발생한 touch event가 point안에 있었는 지 판단한다.
만약 여기서 false를 리턴하면 해당 뷰는 터치를 받지 않는다.
반대로 말하면 point 함수를 조작해서 touch 범위를 넓힐 수 있다는 뜻 ?
2. hittest 함수
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
이 함수는 위 point가 true일 때 실행된다.
만약 touch event가 범위안에 있다고 검증이 되면 터치된 위치에서 가장 적절한 뷰를 찾는다.
nil을 반환할 경우 다음 view를 찾으로 재귀적으로 이동한다.
여기를 조작하면 특정 뷰만 터치를 받게 조잙할 수도 있다는 뜻 같다.
그러면 view2(파란색 뷰)만 받게 하고 나머지 2개는 못받게 만들어보자.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view2
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
print("Touch 시작")
guard let event = event else { return }
// 1. UIEvent 정보
event.allTouches?.forEach({ touch in
print("view2에 눌렸는가?",touch.view == view2)
})
}
위 처럼 모든 touch 이벤트를 view2에게 할당하게하고 3개의 뷰를 모두 하나씩 눌러봤다

재대로 터치 이벤트 발생 최상위 뷰를 view2로 재대로 인식이된다.
그렇다면 이번에는 동작원리를 알아보자.
동작원리
위 구조를 간단하게 나타면 이런 tree 구조이다.

이 때 hitTest를 통해 최상위 뷰를 찾아내는 순서는 다음과 같다.
이 때 사용되는 알고리즘이 reverse-pre-order DFS다.
일반적으로 pre-order는 Root->Left->Right인데 reverse가 붙어 Root->Right->Left로 간다.
이유는 가장 하단 view부터 탐색하기 위함인 것 같다.
DFS로 가장 아래인 view3으로 간 후 view2 이후 hitTest가 nil이면
view1으로 touch event가 전달된다.

위 트리가 이해 되면 이 내용을 한번 혼자 생각해보자.

4. UIResponder
UIEvent를 받아 처리하기 위한 추상 인터페이스
UIResponder가 이벤트를 받아서 처리한다 ??
우리는 지금까지 이벤트를 잘 처리해왔지만 UIResponder를 만들어 본 적이 없는데 애플이 거짓말을 하고 있는 것인가 ??
정답은 전혀 그렇지 않다. 아래 코드를 봐보자.
@MainActor open class UIViewController : UIResponder
@MainActor open class UIView : UIResponder
UIWindow : UIView
@MainActor open class UIApplication : UIResponder
class AppDelegate: UIResponder,
class SceneDelegate: UIResponder
보면 UIViewController, UIView, UIWindow, AppDelegate, SceneDelegate 보면 모두
UIResponder를 상속 받고 있다.
UIResponder는 총 2가지 역할을한다.
- UIEvent 객체를 받아서 처리한다.
- 해당 UIResponder가 이벤트를 처리를 하지 않으면 다음 UIResponder에게 UIEvent 객체 처리를 넘긴다.
여기서 다음 UIResponder는 어떻게 알 수 있을까 ??
여기서 등장하는 개념이 이번 주제인 UIResponder Chain이다.
5. UIResponder Chain
Chain은 사슬로 무엇인가 연결되어있는 느낌인 것 같다.
Chain의 형태

- view
- superView
- viewController의 최상위 view
- viewController
- window
- UIApplication ..
순서로 진행 되는 것을 볼 수 있다.
다음으로 넘길 때는 UIResponder의 next 프로퍼티를 통해 다음 UIResponder가 있는 지 알 수 있다.

✅ hitTest와 UIResponder Chain 차이
- hitTest(_:with:) → 터치된 뷰를 찾아줌 (뷰 탐색)
- Responder Chain → 터치 이벤트를 전달함 (이벤트 전파)
UIView (A)
├── UIView (B)
│ ├── UIButton (C) ← 터치됨!
│
├── UIView (D)
✔️ 터치가 C에서 발생하면
1️⃣ A.hitTest() → B 탐색 → C 탐색 → C가 최종 반환
2️⃣ C가 touchesBegan(_:with:)을 구현했다면 실행
3️⃣ C가 구현하지 않았다면 B.next(B)로 이벤트 전달
4️⃣ B도 touchesBegan을 구현하지 않았다면 A.next(A)로 전달
5️⃣ 마지막으로 UIWindow → UIApplication으로 올라감
FirstResponder
최초의 Responder

UIResponder의 property와 method를 보면 first responder에 관한 내용이 많은 것을 볼 수 있다.
여기서 자주 쓰이는 becomeFirstResponder와 resignFirstResponder 함수를 학습해보자.
becomFirstResponder
Asks UIKit to make this object the first responder in its window.
말 그대로 호출한 responder가 window first responder가 된다는 뜻이다.
확 와닿지 않는 설명이다. 대표적인 예를 살펴보자.
UITextField를 클릭 시 UITextField는 window의 first responder가 되는데 추측으로는 UITextField가
first responder가 되면 내부적으로 keyboard가 올라오는 동작이 있는 것 같다.
만약 특정 이벤트 시 어떤 responder에게 first responder를 지정하고 싶을 때 사용한다.
resignFirstResponder
becomeFirstResponder와 정확히 반대 되는 동작을 한다.
window의 최초 리스폰더로서의 상태를 그만한다고 요청을 받았음을 알린다.
그러므로 UITextField에서 resignFirstResponder()를 호출하면 키보드가 사라진다.
UITouch 설명 중 나온 표를 보며 이벤트 별 어떤 것이 first responder가 되는 지 다시한번 살펴보자.
5. UIControl
UIControl은 간단히 설명하자면 특정 액션이나 사용자의 의도(드래그, 버튼 클릭 등등)를 전달하는
시각적인 요소들의 기반이 되는 클래다.
UIControl 클래스를 상속하는 클래스로는 대표적으로 UIButton 클래스가 있다.
UIButton의 문서를 UIControl을 상속한다고 명시되어 있다.
@MainActor open class UIButton : UIControl
즉 사용자에게 보여지는 뷰가 보다 사용자와 상호작용할 수 있게끔 능력을 부여해주는 클래스이다.
UIControl 은 Target-Action이라는 매커니즘을 이용해 사용자의 액션들을 앱에 전달한다.
이 Target-Action 매커니즘은 addTarget((_:action:for:)) 메서드를 이용하여 구현한다.
파라미터로 액션을 담당할 객체, 액션에 대한 행위를 정의해준 메서드, 그리고 어느 액션(.touchUpInsider, .valueChanged 등)에 대해 해당 메서드를 호출할 것인지를 넘겨준다.
또한 UIControl 클래스의 속성에는 상태라는 속성이 존재하는데 이 상태는 해당 뷰의 모습과
사용자의 액션에 대한 기능을 결정하는 역할을 한다.
이 상태라는 속성은 사용자의 액션에 대해 원하는대로 직접 구현해줄 수 있다.
당연히 이렇게 설명을 하면 와닿지 않을 수 있습니다. 예를 보자.
위에서 언급했듯이 UIControl을 상속받는 대표적인 뷰는 바로 UIButton 클래스다.
UIButton를 보면 다음과 같은 메소드들을 많이 봤을 것이다.
open func setTitle(_ title: String?, for state: UIControl.State)
open func setTitleColor(_ color: UIColor?, for state: UIControl.State)
open func setTitleShadowColor(_ color: UIColor?, for state: UIControl.State)
open func setImage(_ image: UIImage?, for state: UIControl.State)
open func setBackgroundImage(_ image: UIImage?, for state: UIControl.State)
두번 째 매개 변수로 state를 입력 받는 것을 볼 수 있다.
UIControl을 상속 받는 다면 위와 같이 특정 상태에 따른 별도의 동작을 쉽게 가능케 한다.
참고
'iOS > UIKit' 카테고리의 다른 글
생명주기 (4) [ 업데이트 Cycle ] (2) | 2024.09.01 |
---|---|
생명주기 (3) [ View 생명주기 ] (0) | 2024.09.01 |
UIKit 코드 베이스 셋팅 (0) | 2024.09.01 |
생명주기 (2) [ ViewController 생명주기 ] (0) | 2024.08.31 |
생명주기 (1) [ iOS 앱 생명주기, Scene 생명주기 ] (2) | 2024.08.31 |