-
Notifications
You must be signed in to change notification settings - Fork 0
[DropDown] View 외부를 클릭했을 경우 dismiss 되도록 기능을 구현해보자
🏠 동네한입앱에서 dropDown을 개선하는 도중 엄청난 삽질을 했던 경험을 기록한 것입니다 😭
동네한입 팀은 협업을 위해 공통 UI 컴포넌트를 만들어서 View작업을 진행하고 있습니다 😀
선영님이 DropDown View를 구현해주셔서 테스트를 하던 도중 DropDown View 외부를 클릭했을 때 dismiss 되지 않아 사용성에 불편함을 느껴 개선해야겠다고 생각하였습니다
무슨 방법으로 기능 구현할지 감이 잘 오지않아서 키보드의 endEditing과 비슷한 flow로 구현하면 되지 않을까 싶어 열심히 학습하였는데 keyboard는 window를 만들어내서 동작하는 것을 알게되었고 window까지 접근해야하는 게 맞을까 싶어서..
지인분인 현업자분들께 질문드렸습니다 😎
- 바깥을 Tab하는 이벤트를 만들어서 tab한 위치가 dropdown 바깥일 경우 dismiss 처리
- hitTest로 point와 이벤트를 받아와서 분기하여 dismiss 처리
두분에게 질문드렸으니 두가지 방법을 다 진행해봐야겠죠.. 🥹
Tab하는 이벤트를 만들기 위해 ViewController내에서 TouchesBegan을 가지고 시도하였습니다
근데…. 저희 ViewController 구조상 scrollView가 들어가있어서 touchesBegan이 작동하지 않았었습니다 🥲 (한시간 삽질 추가!)
그래서 tapGesture를 만들어 view에다가 추가를 해주었습니다
func setupTapGesture() {
let backViewTap = UITapGestureRecognizer(target: self, action: #selector(backViewTapped(_:)))
backViewTap.cancelsTouchesInView = false
view.addGestureRecognizer(dimmedTap)
}
하지만 여기서 중요한 것은 cancelsTouchesInView입니다!!
cancelsTouchesInView 제스처가 인식될 때 뷰에 터치가 전달되는지 여부를 결정하는 Bool값입니다.
이 프로퍼티의 기본값은 true인데 true인 경우 gesture 이전에 전달된 터치는 취소하게 됩니다..
이 프로퍼티 때문에 dropDown내 tableView 터치가 취소되어 삽질이 한시간 추가되었습니다… 🥲
저는 UIView의 point 메서드를 사용해서 tapGestureRecognizer의 location과 비교하는 로직으로 기능을 구현하였습니다
point(inside: with:) receiver에 지정된 point가 포함되어 있는지 여부를 나타내는 Bool값을 반환합니다
해당 point 메서드를 이용해서 backViewTappedLocation이 dropDown의 point에 포함되어있는지 판별할 수 있습니다
@objc private func backViewTapped(_ tapRecognizer: UITapGestureRecognizer) {
let backViewTappedLocation = tapRecognizer.location(in: self.dropDown)
if dropDown.point(inside: backViewTappedLocation, with: nil) == false {
dropDown.dismiss()
}
}
hitTest를 학습한 적이 있긴하지만 오래되어서 hitTest를 알아봅시다
hitTest(_:with:) 지정된 point를 포함하는 뷰 계층 구조(자신 포함)에서 receiver의 가장 먼 하위 항목을 반환합니다.
IOS에서는 터치이벤트를 수신하는 가장 앞쪽 UIView를 결정하기 위해서 hitTesting을 사용합니다
그렇다면 저희 동네한입 app에서는 DropDown를 클릭했을때 가장 최상단 View가 되는것입니다
저는 DropDown의 hitTest를 override해서 현재 hitTest한 point가 dropDown의 point내부에 있다면 dropDownView를 first responder로 만들었고
point 바깥을 클릭한다면 nil값을 return하여 superView가 터치이벤트를 받도록 설정하여 dismiss 처리하도록 구현을 하였습니다
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let selfHitView = super.hitTest(point, with: event)
if self.point(inside: point, with: event) == true {
return selfHitView
} else {
self.dismiss()
return nil
}
}
하지만 이 방법은 내부에서 객체 자신이 dismiss되는 로직을 구현하다보니 외부에서 해당작업을 명시하지 않아 flow를 파악하기 어렵다고 생각하여 HitTest방식보다는 tapGesture를 이용한 방식으로 프로젝트에 적용할 계획입니다
해당 기능을 구현하면서 객체의 역할과 이벤트 flow에 대해서 고민할 수 있는 시간을 가지게 되었고
point를 활용하여 이벤트 처리를 하는 방식에 대해 공부할 수 있게 되었습니다 🙂
도움을 주신 올라프, 데니에게 감사드립니다
[참고]