iOS - Dismiss presented view controller touching outside its View

Ivan Cantarino picture Ivan Cantarino · Jul 3, 2017 · Viewed 7.4k times · Source

I have a CustomPresentationController which animates in and out with custom animations;

This specific controller gets presented, more less at 50% of the screen size, and when I present it, I add a shadow-gray view to the presentingViewController so it adds some depth.

I can only dismiss the presentedViewController if I tap the cancel button in the NavBar which I call the default dismiss(:) method.

What I'm trying to accomplish is to detect a tap outside the presentedViewController, maybe inside the gray zone, so I can dismiss the presentedViewController, somehow like dismissing an ActionSheet but I've failed to do it. Let me explain what I've tried so far.

I tried to add a UITapGestureRecognizer to the shadow-gray view but since I'm presenting a different controller, the app-engine might think that since the shadow view isn't on the top hierarchy view it might not be accessible so it 'blocks' the recognizer - whenever I tap it, the gesture handles doesn't fire.

I'm implementing now in addition a swipe down to dismiss, which I can make it easily, but I really wanted the tap-outside feature to work as well.

Any hint on how can I approach this?

The apps image is the following:

screenshot

Answer

Mathieu Delehaye picture Mathieu Delehaye · Aug 3, 2020

My solution:

In presenting view controller (aka ViewControllerA):

let storyboard = UIStoryboard(name: "Main", bundle: nil)
                
let vcb = storyboard.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB // ViewControllerB is the presented view controller 
        
vcb.modalPresentationStyle = .custom
        
vcb.transitioningDelegate = self

modalRatio = Float(0.5) // modalRatio is an object property 
   
self.present(pvc, animated: true)

ViewControllerA shall also implement Transitioning delegate:

extension ViewControllerA: UIViewControllerTransitioningDelegate {
        
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
                
        return PartialSizePresentController(presentedViewController: presented, presenting: presenting, withRatio: modalRatio ?? 0.5) // modal ratio is configurable using modalRatio property
        
    }
    
}

Then, implement the presentation controller (aka PartialSizePresentController), so that it also handles tap gesture:

class PartialSizePresentController: UIPresentationController {
    
    let heightRatio : CGFloat
    
    init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, withRatio ratio: Float = 0.5) {
        
        heightRatio = CGFloat(ratio)
        
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        
    }

    override var frameOfPresentedViewInContainerView: CGRect {
        
        guard let cv = containerView else { fatalError("No container view available") }
        
        return CGRect(x: 0, y: cv.bounds.height * (1 - heightRatio), width: cv.bounds.width, height: cv.bounds.height * heightRatio)
        
    }
    
    override func presentationTransitionWillBegin() {
        
        let bdView = UIView(frame: containerView!.bounds)
        
        bdView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        
        containerView?.addSubview(bdView)
        
        bdView.addSubview(presentedView!)
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PartialSizePresentController.handleTap(_:)))
        
        bdView.addGestureRecognizer(tapGesture)
        
    }
    
    @objc func handleTap(_ sender: UITapGestureRecognizer) {
        presentedViewController.dismiss(animated: true, completion: nil)
    }
}