class CustomPannableView: UIView { // MARK: Properties private typealias BottomSheetStyle = StyleConstants.Views.BottomSheet private var minimumVelocityToHide = BottomSheetStyle.minimumPanningVelocityToAutoHide private var minimumScreenRatioToHide = BottomSheetStyle.minimumPositionRatioToAutoHide private var animationDuration = BottomSheetStyle.animationDuration private var parentView: UIView? private var completion: (() -> ())? // MARK: Gesture func setupPanGesture(within parent: UIView, completion: (() -> ())?) { self.parentView = parent self.completion = completion let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) addGestureRecognizer(panGesture) } private func slideViewVertically(to y: CGFloat, animated: Bool = false, withCompletion: Bool = false) { if animated { UIView.animate(withDuration: animationDuration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseInOut, animations: { self.parentView?.frame.origin = CGPoint(x: 0, y: y) }, completion: { (isCompleted) in if withCompletion && isCompleted { self.completion?() } }) return } else { self.parentView?.frame.origin = CGPoint(x: 0, y: y) if withCompletion { self.completion?() } } } @objc private func onPan(_ panGesture: UIPanGestureRecognizer) { guard let parent = self.parentView else { return } switch panGesture.state { // slide view to follow touch on panning begin/continuation case .began, .changed: let translation = panGesture.translation(in: parent) if translation.y >= 0 { slideViewVertically(to: translation.y) } // close view if should close, // reset view if otherwise case .ended: let translation = panGesture.translation(in: parent) let velocity = panGesture.velocity(in: parent) // determine based on final touch position or velocity let shouldClose = (translation.y > parent.frame.size.height * minimumScreenRatioToHide) || (velocity.y > minimumVelocityToHide) if shouldClose { self.slideViewVertically(to: parent.frame.size.height, animated: true, withCompletion: true) } else { self.slideViewVertically(to: 0, animated: true, withCompletion: false) } // reset view for undefined pan gesture state default: self.slideViewVertically(to: 0, animated: true, withCompletion: false) } } }