본문 바로가기

프로그래밍/iOS,macOS

UIPanGestureRecognizer 슬라이드 다운 뷰

액션시트와 같이 하단에 표시되는 뷰를 pan gesture를 사용해 에니메이션 시키고, 사라지게 하는 예.

class DefaultTouchPanGestureRecognizer: UIPanGestureRecognizer {
   var touchPosition: CGPoint?
   
   override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
      super.touchesBegan(touches, with: event)
      touchPosition = touches.first?.location(in: view)
   }
}

class SampleViewController: UIViewController {
   private var panGesture: DefaultTouchPanGestureRecognizer = .init()
   private var firstPanPoint: CGPoint = .zero
   let disposeBag: DisposeBag = .init()
   
   let menuView: UIView = .init()

   override func loadView() {
      super.loadView()
      panGesture.delegate = self
      panGesture.rx
         .event
         .subscribe(onNext: { [weak self] gesture in
            self?.panning(gesture: gesture)
         })
         .disposed(by: disposeBag)
   }
}

 

delegate 설정. 가로방향은 필요 없으므로, 제외시킨다.

extension SampleViewController: UIGestureRecognizerDelegate {
   public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
      guard let view = touch.view else { return }
      return (view is UIControl) == false
   }
   
   public func gestureRecognizerShouldBegan(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
      guard let panGestureRecognizer = gestureRecognizer as? DefaultTouchPanGestureRecognizer else { return true }
      
      let velocity = panGestureRecognizer.velocity(in: panGestureRecognizer.view?.superview)
      if abs(velocity.y) > abs(velocity.x) {
         return true
      } else {
         return flase
      }
   }
}

 

pan 이벤트 처리

private func panning(gesture: UIPanGestureRecognizer) {
   let point = gesture.translation(in: gesture.view?.superview)

   if gesture.state == .began {
	   firstPanPoint = point
   }

   let height = menuView.frame.height
   var offset: CGFloat = 0

   let newHeight = max(0, height + (firstPanPoint.y - point.y))
   if newHeight < height {
      offset = height - newHeight
   }

   if gesture.state == .cancelled || gesture.state == .failed {
      UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut], animations: { [weak self] in
         self.menuView.transform = .identity
      }, completion: nil)
   } else if gesture.state == .ended {
      let velocity = (0.2 * gesture.velocity(in: view).y)
      var finalOffset = height - offset - velocity
      if velocity > 500 {
         finalOffset = 0
      }

      let duration: CGFloat = abs(velocity / 10_000) + 0.2
      let animationDuration = TimeInterval(duration)

      if finalOffset > (height / 2) {
         UIView.animate(withDuration: animationDuration, delay:0, options: [.curveEaseOut], animations: { [weak self] in
            self.menuView.transform = .identity
         }, completion: nil)
       } else {
         UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseOut], animations: { [weak self] in
            self?.menuView.transform = CGAffineTransform(translationX: 0, y: height)
            self?.view.backgroundColor = .clear
         }, completion: { [weak self] _ in
            self?.dismiss(animated: false, completion: nil)
         })
      }
   } else {
      if offset > 0 {
         menuView.transform = CGAffineTransform(translationX: 0, y: offset)
      } else {
         menuView.transform = .identity
      }
   }
}