Disable the Interactive Dismissal of Presented View Controller

Disable the interactive dismissal of presented view controller

Option 1:

viewController.isModalInPresentation = true

Disabled interactive dismissal

(Disabled interactive .pageSheet dismissal acts like this.)

  • Since the iOS 13, UIViewController contains a new property called isModalInPresentation which must be set to true to prevent the interactive dismissal.
  • It basically ignores events outside the view controller's bounds. Bear that in mind if you are using not only the automatic style but also presentation styles like .popover etc.
  • This property is false by default.

From the official docs: If true, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.



Option 2:

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
  • Since the iOS 13, UIAdaptivePresentationControllerDelegate contains a new method called presentationControllerShouldDismiss.
  • This method is called only if the presented view controller is not dismissed programmatically and its isModalInPresentation property is set to false.

Tip: Don't forget to assign presentationController's delegate. But be aware, it is known that even just accessing the presentationController can cause a memory leak.

Disable gesture to pull down form/page sheet modal presentation

In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.

If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.

If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:

  1. If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.

  2. If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

  3. If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.

I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.

Prevent dismissal of modal view controller in SwiftUI

Update for iOS 15

As per pawello2222's answer below, this is now supported by the new interactiveDismissDisabled(_:) API.

struct ContentView: View {
@State private var showSheet = false

var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}

Pre-iOS-15 answer

I wanted to do this as well, but couldn't find the solution anywhere. The answer that hijacks the drag gesture kinda works, but not when it's dismissed by scrolling a scroll view or form. The approach in the question is less hacky also, so I investigated it further.

For my use case I have a form in a sheet which ideally could be dismissed when there's no content, but has to be confirmed through a alert when there is content.

My solution for this problem:

struct ModalSheetTest: View {
@State private var showModally = false
@State private var showSheet = false

var body: some View {
Form {
Toggle(isOn: self.$showModally) {
Text("Modal")
}
Button(action: { self.showSheet = true}) {
Text("Show sheet")
}
}
.sheet(isPresented: $showSheet) {
Form {
Button(action: { self.showSheet = false }) {
Text("Hide me")
}
}
.presentation(isModal: self.showModally) {
print("Attempted to dismiss")
}
}
}
}

The state value showModally determines if it has to be showed modally. If so, dragging it down to dismiss will only trigger the closure which just prints "Attempted to dismiss" in the example, but can be used to show the alert to confirm dismissal.

struct ModalView<T: View>: UIViewControllerRepresentable {
let view: T
let isModal: Bool
let onDismissalAttempt: (()->())?

func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}

func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
context.coordinator.modalView = self
uiViewController.rootView = view
uiViewController.parent?.presentationController?.delegate = context.coordinator
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
let modalView: ModalView

init(_ modalView: ModalView) {
self.modalView = modalView
}

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
!modalView.isModal
}

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
modalView.onDismissalAttempt?()
}
}
}

extension View {
func presentation(isModal: Bool, onDismissalAttempt: (()->())? = nil) -> some View {
ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)
}
}

This is perfect for my use case, hope it helps you or someone else out as well.

Swift - Interactive dismiss of View Controller completion handler

Your viewDidDisappear() will do the work for this as it will only be called for the viewController that you are dismissing.

You can try using the deinit() function as well.

You can also track this property on your viewController

var isMovingToParent: Bool

A Boolean value indicating whether the view controller is being moved
to a parent view controller.

Or you can show your controller full screen while instantiating it.

For example,

 @objc fileprivate func continueButtonDidPress(){
//TODO:- Go to Feed
let viewController = MainViewController()
viewController.modalTransitionStyle = .flipHorizontal
viewController.modalPresentationStyle = .fullScreen
present(viewController, animated: true, completion: nil)
}

How disable drag dismiss in UIViewController

You need disable your pan gesture to disable drag dismiss.

Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?

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).

  1. Firstly we define OverlayViewController which will be later superclass of presented SecondViewControllerand will contain UIPanGestureRecognizer.

       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)
    }
  2. Next we define custom PresentationController

       class 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
    }
  3. Create SecondViewController subclassing OverlayViewController:

     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)
    }
  4. Add showOverlay() function that will be triggered after buttonPressed and conform your presenting UIViewController (TestViewController) to UIViewControllerTransitioningDelegate :

     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
    }
    }
  5. Now we should be able to present SecondViewController with showOverlay() function setting its presentedHeight to 1.0 and .dim background effect. We can dismiss SecondViewController similar to another modal presentations.



Related Topics



Leave a reply



Submit