이전 포스팅에선, Custom DropDown을 구현했었다. 

 

우선, DropDownView를 살펴보면 다음과 같다. 

  • "AnchorView를 터치"하게 되면 DropDownView는 TableView를 display 한다. 
  • "TableView에서 옵션을 선택"하거나, "외부 영역을 터치"하면 TableView를 hide 한다.

기존의 로직에선, ViewController가 터치 이벤트의 발생한 영역을 다음과 같이 판단했어야 했다. 

/// ViewController
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
	super.touchesBegan(touches, with: event)
	guard
		let touch = touches.first,
		let hitView = self.view.hitTest(touch.location(in: view), with: event)
	else { return }

	self.hit(at: hitView)
}

func hit(at hitView: UIView?) {
	guard let hitView = hitView else { return }
	
	dropDownViews?.forEach { view in
		if
			view.anchorView === hitView {
			view.isDisplayed.toggle()
		} else {
			view.isDisplayed = false
		}
	}
}

우선, touchesBegan에서 touch가 발생된 view를 hit 메서드로 넘겨주고, 

hit메서드에선, 터치가 발생된 view가 anchorView인지 아닌지 판별한다. 

 

즉, DropDownView가 사용되는 모든 ViewController는 "외부 터치인지 내부 터치인지 판별"해야 한다. 

이러한 공통된 로직을 작성해야 하기 때문에, DropDownView의 사용성이 떨어진다고 판단했다. 

이번 포스팅에선 UIResponder의 firstResponder를 활용해 이를 개선해 보자. 

개선된 결과물을 여기서 확인해 볼 수 있다.

https://github.com/jungseokyoung-cloud/iOS-Study/tree/main/CustomDropDownV2

 

iOS-Study/CustomDropDownV2 at main · jungseokyoung-cloud/iOS-Study

iOS Study Archive. Contribute to jungseokyoung-cloud/iOS-Study development by creating an account on GitHub.

github.com

 

 

firstResponder란? 

"firstResponder"란 이벤트가 발생했을 때 가장 처음 이벤트를 전달받는 객체이다.

이때, 해당 View에서 이벤트를 처리하지 않으면, Responder Chain에 있는 다음 Responder 객체로 전달된다. 

 

이 firstResponder는 개발자가 직접 지정해 줄 수 있다.

motion 이벤트와 같은 특정 이벤트는 지정된 firstResponder로 이벤트를 처음 전달된다. 

 

반면, 터치 이벤트의 경우에는 UIKit이 터치가 발생한 View를 firstResponder로 지정한다.

따라서, 개발자가 firstResponder를 지정해 줘도 touch가 발생한 view로 이벤트가 전달된다. 

 

UIResponder와 firstResponder에 대한 자세한 내용을 알고 싶다면, 해당 포스팅을 참고 바란다. 

 

inputView 

만약, 특정 view가 firstResponder가 되어 있는 동안, inputView를 화면에 표시한다. 

대표적인 예시로는 UITextField로, firstResponder가 되면 inputView인 키보드를 표시한다. 

 

더욱 자세한 내용은 여기를 참고 바란다. 

 

 

DropDownView 아이디어

DropDownView에서 inputView를 TableView로 지정하게 되면, firstResponder만 관리하면 된다.

하지만, inputView는 Screen 하단부에 고정되어 있으며, 높이를 제외한 다른 프로퍼티를 조작할 수 없었다. 

 

inputView를 직접적으로 활용할 순 없지만, TableView를 inputView처럼 활용하고자 한다.

 

기본적인 아이디어는 다음과 같다. 

  • "AnchorView가 touch"되면, DropDownView를 firstResponder로 지정한다.
  • "firstResponder가 되면", DropDownView는 TableView를 display 된다. 
  • "firstResponder가 resign"되면, TableView를 hide 한다.

이렇게 구현하게 되면, ViewController에선 다음과 같은 코드 한 줄로 TableView를 hide 할 수 있게 된다.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?){
    self.view.endEditing(true)
}
endEditing 메서드는 subView에서 firstResponder가 있으면, resign 시킨다.

 

 

DropDownView 리팩토링 

위에 아이디어를 통해, DropDownView를 리팩토링 해보자. 

 

override var canBecomeFirstResponder: Bool { true }

/// DropDownView의 상태를 확인하는 private 변수입니다.
private var dropDownMode: DropDownMode = .hide

private enum DropDownMode {
	case display
	case hide
}

우선, canBecomeFirstResponder를 오버라이드해주고,  

내부적으로 dropDown의 모드를 나타내는 변수를 선언해 준다. 

 

다음으로, becomeFirstResponder()resignFirstResponder()메서드를 오버라이드해주자.

@discardableResult
override func becomeFirstResponder() -> Bool {
	super.becomeFirstResponder()

	dropDownMode = .display
	displayDropDown(with: dropDownConstraints)
	return true
}

@discardableResult
override func resignFirstResponder() -> Bool {
	super.resignFirstResponder()

	dropDownMode = .hide
	hideDropDown()
	return true
}

 

마지막으로 touchesBegan 메서드를 오버라이드 해주자. 

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
	if dropDownMode == .display {
		resignFirstResponder()
	} else {
		becomeFirstResponder()
	}
}

 

이때, super.touchesBegan(touches, with: event)를 호출하지 않아 다음 responder로 이벤트를 전달하지 않는다. 

 

이렇게 되면, DropDownView의 상위 Responder인 ViewController는 이벤트를 전달받지 않게 된다. 

즉, ViewController가 받는 터치 이벤트는 모두 DropDownView 외부 터치가 된다. 

 

기존에는 ViewController에선 "외부터치인지 내부터치인지 판별"을 위해 10줄 이상의 추가적인 코드를 작성했어야 했지만,

UIResponder를 활용하게 되면서, 외부터치인지 판별할 필요가 없게 되어 다음과 같은 코드 한 줄로 개선되었다.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
	view.endEditing(true)
}

 

'iOS > iOS' 카테고리의 다른 글

[iOS] Custom Calendar 구현(2)  (1) 2024.05.15
[iOS] Custom Calendar 구현(1) - UI 및 달력 데이터  (1) 2024.05.15
[iOS] Core Animation(2) - CAAnimation  (0) 2024.03.07
[iOS] Core Animation(1) - Concept  (0) 2024.03.05
[iOS] Render Loop & Hitch(2)  (1) 2024.02.24
복사했습니다!