Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save RaajeevChandran/5967b9b61e696c80237268c18e1e4084 to your computer and use it in GitHub Desktop.

Select an option

Save RaajeevChandran/5967b9b61e696c80237268c18e1e4084 to your computer and use it in GitHub Desktop.
import UIKit
class BottomSheetPresentationController: UIPresentationController {
private let dismissThreshold: CGFloat = 0.3
private let grabberView: UIView = {
let view = UIView()
view.frame.size = CGSize(width: 50, height: 3)
view.backgroundColor = .lightGray
return view
}()
private let dimmedBackgroundView: UIView = {
let view = UIView()
view.isUserInteractionEnabled = true
return view
}()
private lazy var panGesture = UIPanGestureRecognizer(target: self, action: #selector(pannedPresentedView))
private lazy var tapGestureToDismiss = UITapGestureRecognizer(target: self, action: #selector(tappedDimmedBackgroundView))
private var transitioningDelegate: BottomSheetTransitioningDelegate? {
presentedViewController.transitioningDelegate as? BottomSheetTransitioningDelegate
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView, let presentedView else {
return super.frameOfPresentedViewInContainerView
}
let maximumWidth = containerView.bounds.width
let maximumHeight = containerView.frame.height - containerView.safeAreaInsets.top - containerView.safeAreaInsets.bottom
let fittingSize = CGSize(width: maximumWidth, height: UIView.layoutFittingCompressedSize.height)
let presentedViewSize = presentedView.systemLayoutSizeFitting(
fittingSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .defaultLow
)
let presentedViewHeight = presentedViewSize.height
let targetHeight = presentedViewHeight == .zero ? maximumHeight : presentedViewHeight
let finalHeight = min(targetHeight, maximumHeight) + containerView.safeAreaInsets.bottom
let finalSize = CGSize(width: maximumWidth, height: finalHeight)
let finalOrigin = CGPoint(x: .zero, y: containerView.frame.maxY - finalSize.height)
return CGRect(origin: finalOrigin, size: finalSize)
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.addSubview(dimmedBackgroundView)
dimmedBackgroundView.backgroundColor = .black.withAlphaComponent(0.5)
presentedView?.addSubview(grabberView)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: {
[weak self] _ in
guard let self else { return }
self.presentedView?.layer.cornerRadius = 25
})
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
guard let presentedView, let containerView else {
return
}
dimmedBackgroundView.frame = containerView.bounds
grabberView.frame.origin.y = 8
grabberView.center.x = presentedView.center.x
grabberView.layer.cornerRadius = grabberView.frame.height / 2
setupInteraction()
presentedView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
presentedViewController.additionalSafeAreaInsets.top = grabberView.frame.maxY
}
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
guard let self else { return }
self.presentedView?.layer.cornerRadius = .zero
self.dimmedBackgroundView.backgroundColor = .clear
})
}
}
// MARK: Pan Gesture
extension BottomSheetPresentationController {
private func setupInteraction() {
guard let presentedView = presentedView else {
return
}
func addGestureIfNeeded(_ gesture: UIGestureRecognizer, to view: UIView, delegate: UIGestureRecognizerDelegate? = nil) {
if let recognizers = view.gestureRecognizers, recognizers.contains(gesture) {
return
}
gesture.delegate = delegate
view.addGestureRecognizer(gesture)
}
addGestureIfNeeded(panGesture, to: presentedView, delegate: self)
addGestureIfNeeded(tapGestureToDismiss, to: dimmedBackgroundView)
}
private func dismiss(withInteraction: Bool) {
transitioningDelegate?.transition.wantsInteractiveStart = withInteraction
presentedViewController.dismiss(animated: true)
}
@objc private func pannedPresentedView(_ recognizer: UIPanGestureRecognizer) {
guard let presentedView = presentedView else {
return
}
switch recognizer.state {
case .began:
transitioningDelegate?.transition.wantsInteractiveStart = true
case .changed:
let translation = recognizer.translation(in: presentedView)
didGestureTranslationChange(translation)
case .ended:
let translation = recognizer.translation(in: presentedView)
let progress = translation.y / presentedView.frame.height
if progress > dismissThreshold {
dismiss(withInteraction: true)
transitioningDelegate?.transition.finish()
} else {
UIView.animate(withDuration: 0.2) {
presentedView.transform = .identity
}
transitioningDelegate?.transition.cancel()
}
case .cancelled, .failed:
break
default:
break
}
}
private func didGestureTranslationChange(_ translation: CGPoint) {
guard let presentedView = presentedView else {
return
}
let translationY = max(0, translation.y)
presentedView.transform = CGAffineTransform(translationX: 0, y: translationY)
let progress = translationY / presentedView.frame.height
transitioningDelegate?.transition.update(progress)
}
private func didInteractionEnd() {
guard let presentedView = presentedView else {
return
}
if let transitioningDelegate = transitioningDelegate {
let dismissFraction = transitioningDelegate.transition.dismissFractionComplete
if dismissFraction > dismissThreshold {
transitioningDelegate.transition.finish()
presentedViewController.dismiss(animated: true)
} else {
transitioningDelegate.transition.cancel()
UIView.animate(withDuration: 0.3) {
presentedView.transform = .identity
}
}
} else {
UIView.animate(withDuration: 0.3) {
presentedView.transform = .identity
}
}
}
@objc private func tappedDimmedBackgroundView() {
dismiss(withInteraction: false)
}
}
extension BottomSheetPresentationController: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment