Swift
[RxGesture] 컨테이너 뷰와 내부 요소의 제스처 구분하기
JDeoks
2024. 3. 2. 01:24
모달창의 외부를 터치했을 때, 모달창이 닫히는 기능을 구현하려고 한다.
모달창의 외부 뷰(self.view)와 모달창의 제스처를 같이 구독해야 하는데
처음 접근했을 때, 모달창 내부의 요소를 탭해도 self.view가 탭된 것으로 간주되어 모달창이 닫히는 문제가 발생했다.
class SearchPlaceViewController: UIViewController {
let disposeBag = DisposeBag()
@IBOutlet var modalContainerView: UIView!
@IBOutlet var searchTextField: UITextField!
@IBOutlet var seachResultTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
initUI()
action()
}
private func initUI() {
// containerView
containerView.layer.cornerRadius = 16
}
private func action() {
seachResultTableView.rx.tapGesture()
.when(.recognized)
.subscribe(onNext: { _ in
print("seachResultTableView")
})
.disposed(by: disposeBag)
// 창 닫기
self.view.rx.tapGesture()
.when(.recognized)
.subscribe(onNext: { gesture in
print("self.view")
})
.disposed(by: disposeBag)
}
}
처음에 작성했던 코드이다.
당연하게도, 모달창을 누르면 "seachResultTableView"와 "self.view"가 같이 출력되었다.
self.view가 눌렸을 때 제일 앞에 있는 뷰가 self.view가 맞는지 확인하는 코드가 필요했다.
private func action() {
seachResultTableView.rx.tapGesture()
.when(.recognized)
.subscribe(onNext: { _ in
print("seachResultTableView")
})
.disposed(by: disposeBag)
// 창 닫기
self.view.rx.tapGesture()
.when(.recognized)
.subscribe(onNext: { gesture in
/// 제스처 인식기가 추가된 뷰. 여기서는 self.view
guard let targetView: UIView = gesture.view else { return }
let location: CGPoint = gesture.location(in: targetView)
/// 타겟뷰의 터치 위치에서 제일 앞의 뷰
guard let hitView: UIView = targetView.hitTest(location, with: nil) else { return }
if hitView == targetView {
self.dismiss(animated: true, completion: nil)
}
})
.disposed(by: disposeBag)
}
수정한 action 함수.
targetView:
제스처를 구독하고 있는 뷰를 말한다. 위 코드에서는 self.view가 해당
Gesture.location(in: targetView):
사용자가 화면에서 탭한 위치를 targetView의 좌표계를 기준으로 계산하여 반환한다.
HitTest(_:with:):
주어진 포인트(사용자가 탭한 위치)를 기준으로 타겟뷰와 그 서브뷰들을 검사하여, 해당 포인트에서 터치 이벤트를 받을 수 있는 가장 앞에 있는 뷰를 반환한다.
마지막으로 hitView와 targetView가 같을때만 타겟뷰를 dismiss해주면 된다
재사용을 위해 익스텐션으로 추가해주었다.
import Foundation
import UIKit
extension UIView {
/// 현재 뷰가 탭 제스처에 직접 탭되었는지 확인
///
/// `hitTest(_:with:)`를 사용하여 탭 위치에 있는 가장 앞의 뷰를 찾고, 현재 뷰(`self`)와 동일한지 비교.
///
/// - Returns: 현재 뷰가 직접 탭된 경우 true, 그렇지 않으면 (서브뷰가 탭된 경우) false.
///
/// ## 예시
///
/// ``` swift
/// self.view.rx.tapGesture()
/// .when(.recognized)
/// .subscribe(onNext: { gesture in
/// // 내부 요소가 탭되었을 때는 모달이 닫히지 않음
/// if self.view.isTappedDirectly(gesture: gesture) {
/// self.dismiss(animated: true, completion: nil)
/// }
/// })
/// .disposed(by: disposeBag)
/// ```
func isTappedDirectly(gesture: UITapGestureRecognizer) -> Bool {
let location = gesture.location(in: self)
let hitView = self.hitTest(location, with: nil)
return hitView == self
}
}