1

I have made a UIPresentationController that fits any view controller and shows up on half of the screen using this tutorial. Now I would love to add drag to dismiss to this. I'm trying to have the drag feel natural and responsive like the drag experience for "Top Stories" on the Apple iOS 13 stocks app. I thought the iOS 13 modal drag to dismiss would get carried over but it doesn't to this controller but it doesn't.

Every bit of code and tutorial I found had a bad dragging experience. Does anyone know how to do this? I've been trying / searching for the past week. Thank you in advance

Here's my code for the presentation controller

class SlideUpPresentationController: UIPresentationController {
    // MARK: - Variables
    private var dimmingView: UIView!
    
    //MARK: - View functions
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        setupDimmingView()
    }
    
    override func containerViewWillLayoutSubviews() {
      presentedView?.frame = frameOfPresentedViewInContainerView
    }
    
    override var frameOfPresentedViewInContainerView: CGRect {
        guard let container = containerView else { return super.frameOfPresentedViewInContainerView }
        let width = container.bounds.size.width
        let height : CGFloat = 300.0
        
        return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
    }
    
    override func presentationTransitionWillBegin() {
        guard let dimmingView = dimmingView else { return }
        
        containerView?.insertSubview(dimmingView, at: 0)
      
      NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|",
                                                                 options: [],
                                                                 metrics: nil,
                                                                 views: ["dimmingView": dimmingView]))
      
      NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|",
                                                                 options: [],
                                                                 metrics: nil,
                                                                 views: ["dimmingView": dimmingView]))
      
      guard let coordinator = presentedViewController.transitionCoordinator else {
        dimmingView.alpha = 1.0
        return
      }
      
      coordinator.animate(alongsideTransition: { _ in
        self.dimmingView.alpha = 1.0
      })
    }
    
    override func dismissalTransitionWillBegin() {
      guard let coordinator = presentedViewController.transitionCoordinator else {
        dimmingView.alpha = 0.0
        return
      }
      
      coordinator.animate(alongsideTransition: { _ in
        self.dimmingView.alpha = 0.0
      })
    }
    
    func setupDimmingView() {
      dimmingView = UIView()
      dimmingView.translatesAutoresizingMaskIntoConstraints = false
      dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
      dimmingView.alpha = 0.0
      
      let recognizer = UITapGestureRecognizer(target: self,
                                              action: #selector(handleTap(recognizer:)))
      dimmingView.addGestureRecognizer(recognizer)
    }
    
    @objc func handleTap(recognizer: UITapGestureRecognizer) {
      presentingViewController.dismiss(animated: true)
    }
}
fphelp
  • 1,544
  • 1
  • 15
  • 34

1 Answers1

5

As your description about the dragging experience you wanted is not that clear, hope I didn't get you wrong.

I'm trying to have the drag feel natural and responsive like the drag experience for "Top Stories" on the Apple iOS 13 stocks app.

What I get is, you want to be able to drag the presented view, dismiss it if it reach certain point, else go back to its original position (and of coz you can bring the view to any position you wanted). To achieve this, we can add a UIPanGesture to the presentedViewController, then

  1. move the presentedView according to the gesture

  2. dismiss / move back the presentedView

     class SlideUpPresentationController: UIPresentationController {
         // MARK: - Variables
         private var dimmingView: UIView!
         private var originalX: CGFloat = 0
    
         //MARK: - View functions
         override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
             super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
             setupDimmingView()
         }
    
         override func containerViewWillLayoutSubviews() {
             presentedView?.frame = frameOfPresentedViewInContainerView
         }
    
         override var frameOfPresentedViewInContainerView: CGRect {
             guard let container = containerView else { return super.frameOfPresentedViewInContainerView }
             let width = container.bounds.size.width
             let height : CGFloat = 300.0
    
             return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
         }
    
         override func presentationTransitionWillBegin() {
             guard let dimmingView = dimmingView else { return }
    
             containerView?.insertSubview(dimmingView, at: 0)
             // add PanGestureRecognizer for dragging the presented view controller
             let viewPan = UIPanGestureRecognizer(target: self, action: #selector(viewPanned(_:)))
             containerView?.addGestureRecognizer(viewPan)
    
             NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
    
             NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
    
             guard let coordinator = presentedViewController.transitionCoordinator else {
                 dimmingView.alpha = 1.0
                 return
             }
    
             coordinator.animate(alongsideTransition: { _ in
                 self.dimmingView.alpha = 1.0
             })
         }
    
         @objc private func viewPanned(_ sender: UIPanGestureRecognizer) {
             // how far the pan gesture translated
             let translate = sender.translation(in: self.presentedView)
             switch sender.state {
                 case .began:
                     originalX = presentedViewController.view.frame.origin.x
                 case .changed:
                     // move the presentedView according to pan gesture
                     // prevent it from moving too far to the right
                     if originalX + translate.x < 0 {
                         presentedViewController.view.frame.origin.x = originalX + translate.x
                     }
                 case .ended:
                     let presentedViewWidth = presentedViewController.view.frame.width
                     let newX = presentedViewController.view.frame.origin.x
    
                     // if the presentedView move more than 0.75 of the presentedView's width, dimiss it, else bring it back to original position
                     if presentedViewWidth * 0.75 + newX > 0 {
                         setBackToOriginalPosition()
                     } else {
                         moveAndDismissPresentedView()
                     }
                 default:
                     break
             }
         }
    
         private func setBackToOriginalPosition() {
             // ensure no pending layout change in presentedView
             presentedViewController.view.layoutIfNeeded()
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
                 self.presentedViewController.view.frame.origin.x = self.originalX
                 self.presentedViewController.view.layoutIfNeeded()
             }, completion: nil)
         }
    
         private func moveAndDismissPresentedView() {
             // ensure no pending layout change in presentedView
             presentedViewController.view.layoutIfNeeded()
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
                 self.presentedViewController.view.frame.origin.x = -self.presentedViewController.view.frame.width
                 self.presentedViewController.view.layoutIfNeeded()
             }, completion: { _ in
                 // dimiss when the view is completely move outside the screen
                 self.presentingViewController.dismiss(animated: true, completion: nil)
             })
          }
    
         override func dismissalTransitionWillBegin() {
             guard let coordinator = presentedViewController.transitionCoordinator else {
                 dimmingView.alpha = 0.0
                 return
             }
    
             coordinator.animate(alongsideTransition: { _ in
                 self.dimmingView.alpha = 0.0
             })
         }
    
         func setupDimmingView() {
             dimmingView = UIView()
             dimmingView.translatesAutoresizingMaskIntoConstraints = false
             dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
             dimmingView.alpha = 0.0
    
             let recognizer = UITapGestureRecognizer(target: self,
                                           action: #selector(handleTap(recognizer:)))
             dimmingView.addGestureRecognizer(recognizer)
         }
    
        @objc func handleTap(recognizer: UITapGestureRecognizer) {
            presentingViewController.dismiss(animated: true)
        }
    
     }
    

The above code is just an example based on the code you provide, but I hope that explain what's happening under the hood of what you called a drag experience. Hope this helps ;)

Here is the example result:

via GIPHY

paky
  • 595
  • 1
  • 5
  • 18
  • Great experience sideways! Can you adjust it for vertical dismal (swipe down to dismiss)? I'm trying to adjust the code and am having trouble – fphelp Jul 20 '20 at 03:54