'Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?
I want to enable interactive modal dismissal that pans along with a users finger on a fullscreen modally presented view controller .fullscreen.
I've seen that it's fairly trivial to do so on the .pageSheet and the .formSheet which have it built in but have not seen a clear example for the full screen.
I'm guessing I'd need to have a pan gesture added to my vc within the body of it's code and then adjust for the states myself but wondering if anyone knows what exactly needs to be done / if there's a simpler way to do it as it seems much more complicated for the .fullscreen case
Solution 1:[1]
It can be done with creating your custom UIPresentationController and UIViewControllerTransitioningDelegate. Lets say we have TestViewController and we want to present SecondViewController with total presentedHeight of 1.0 (fullScreen). Presentation will be triggered with @IBAction func buttonPressed and can be dismissed by dragging controller down (as we are used to it). It would be also nice to add some backgroundEffect to be gradually changed while user is sliding down the SecondViewController (especially when used only presentedHeight of 0.6).
Firstly we define
OverlayViewControllerwhich will be later superclass of presentedSecondViewControllerand will containUIPanGestureRecognizer.class OverlayViewController: UIViewController { var hasSetPointOrigin = false var pointOrigin: CGPoint? var delegate: OverlayViewDelegate? override func viewDidLoad() { super.viewDidLoad() let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction)) view.addGestureRecognizer(panGesture) } override func viewDidLayoutSubviews() { if !hasSetPointOrigin { hasSetPointOrigin = true pointOrigin = self.view.frame.origin } } @objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) { let translation = sender.translation(in: view) // Not allowing the user to drag the view upward guard translation.y >= 0 else { return } let currentPosition = translation.y let originPos = self.pointOrigin delegate?.userDragged(draggedPercentage: translation.y/originPos!.y) // setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y) if sender.state == .ended { let dragVelocity = sender.velocity(in: view) if dragVelocity.y >= 1100 { self.dismiss(animated: true, completion: nil) } else { // Set back to original position of the view controller UIView.animate(withDuration: 0.3) { self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400) self.delegate?.animateBlurBack(seconds: 0.3) } } } } } protocol OverlayViewDelegate: AnyObject { func userDragged(draggedPercentage: CGFloat) func animateBlurBack(seconds: TimeInterval) }Next we define custom
PresentationControllerclass PresentationController: UIPresentationController { private var backgroundEffectView: UIView? private var backgroundEffect: BackgroundEffect? private var viewHeight: CGFloat? private let maxDim:CGFloat = 0.6 private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer() convenience init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, backgroundEffect: BackgroundEffect = .blur, viewHeight: CGFloat = 0.6) { self.init(presentedViewController: presentedViewController, presenting: presentingViewController) self.backgroundEffect = backgroundEffect self.backgroundEffectView = returnCorrectEffectView(backgroundEffect) self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController)) self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.backgroundEffectView?.isUserInteractionEnabled = true self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer) self.viewHeight = viewHeight } private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { super.init(presentedViewController: presentedViewController, presenting: presentingViewController) } override var frameOfPresentedViewInContainerView: CGRect { CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)), size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height * viewHeight!)) } override func presentationTransitionWillBegin() { self.backgroundEffectView?.alpha = 0 self.containerView?.addSubview(backgroundEffectView!) self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in switch self.backgroundEffect! { case .blur: self.backgroundEffectView?.alpha = 1 case .dim: self.backgroundEffectView?.alpha = self.maxDim case .none: self.backgroundEffectView?.alpha = 0 } }, completion: { (UIViewControllerTransitionCoordinatorContext) in }) } override func dismissalTransitionWillBegin() { self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in self.backgroundEffectView?.alpha = 0 }, completion: { (UIViewControllerTransitionCoordinatorContext) in self.backgroundEffectView?.removeFromSuperview() }) } override func containerViewWillLayoutSubviews() { super.containerViewWillLayoutSubviews() } override func containerViewDidLayoutSubviews() { super.containerViewDidLayoutSubviews() presentedView?.frame = frameOfPresentedViewInContainerView backgroundEffectView?.frame = containerView!.bounds } @objc func dismissController(){ self.presentedViewController.dismiss(animated: true, completion: nil) } func graduallyChangeOpacity(withPercentage: CGFloat) { self.backgroundEffectView?.alpha = withPercentage } func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView { switch effect { case .blur: var blurEffect = UIBlurEffect(style: .dark) if self.traitCollection.userInterfaceStyle == .dark { blurEffect = UIBlurEffect(style: .light) } return UIVisualEffectView(effect: blurEffect) case .dim: var dimView = UIView() dimView.backgroundColor = .black if self.traitCollection.userInterfaceStyle == .dark { dimView.backgroundColor = .gray } dimView.alpha = maxDim return dimView case .none: let clearView = UIView() clearView.backgroundColor = .clear return clearView } } } extension PresentationController: OverlayViewDelegate { func userDragged(draggedPercentage: CGFloat) { graduallyChangeOpacity(withPercentage: 1-draggedPercentage) switch self.backgroundEffect! { case .blur: graduallyChangeOpacity(withPercentage: 1-draggedPercentage) case .dim: graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage) case .none: self.backgroundEffectView?.alpha = 0 } } func animateBlurBack(seconds: TimeInterval) { UIView.animate(withDuration: seconds) { switch self.backgroundEffect! { case .blur: self.backgroundEffectView?.alpha = 1 case .dim: self.backgroundEffectView?.alpha = self.maxDim case .none: self.backgroundEffectView?.alpha = 0 } } } } enum BackgroundEffect { case blur case dim case none }Create
SecondViewControllersubclassingOverlayViewController:class SecondViewController: OverlayViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .blue // Do any additional setup after loading the view. } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() addSlider() } func addSlider() { let sliderWidth:CGFloat = 100 let centerOfScreen = self.view.frame.size.width / 2 let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10) let slider = UIView(frame: rect) slider.backgroundColor = .black self.view.addSubview(slider) }Add
showOverlay()function that will be triggered afterbuttonPressedand conform your presentingUIViewController(TestViewController) toUIViewControllerTransitioningDelegate:class TestViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func buttonPressed(_ sender: Any) { showOverlay() } func showOverlay() { let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController secondVC.modalPresentationStyle = .custom secondVC.transitioningDelegate = self self.present(secondVC, animated: true, completion: nil) } } extension TestViewController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { let presentedHeight: CGFloat = 1.0 let controller = PresentationController(presentedViewController: presented, presenting: presenting, backgroundEffect: .dim, viewHeight: presentedHeight) if let vc = presented as? OverlayViewController { vc.delegate = controller } return controller } }Now we should be able to present
SecondViewControllerwithshowOverlay()function setting itspresentedHeightto1.0and.dimbackground effect. We can dismissSecondViewControllersimilar to another modal presentations.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Mr.SwiftOak |
