In iOS, how to drag down to dismiss a modal?

后端 未结 15 2238
粉色の甜心
粉色の甜心 2020-11-28 00:41

A common way to dismiss a modal is to swipe down - How do we allows the user to drag the modal down, if it\'s far enough, the modal\'s dismissed, otherwise it animates back

相关标签:
15条回答
  • 2020-11-28 00:47

    I'll share how I did it in Swift 3 :

    Result

    Implementation

    class MainViewController: UIViewController {
    
      @IBAction func click() {
        performSegue(withIdentifier: "showModalOne", sender: nil)
      }
      
    }
    

    class ModalOneViewController: ViewControllerPannable {
      override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .yellow
      }
      
      @IBAction func click() {
        performSegue(withIdentifier: "showModalTwo", sender: nil)
      }
    }
    

    class ModalTwoViewController: ViewControllerPannable {
      override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .green
      }
    }
    

    Where the Modals View Controllers inherit from a class that I've built (ViewControllerPannable) to make them draggable and dismissible when reach certain velocity.

    ViewControllerPannable class

    class ViewControllerPannable: UIViewController {
      var panGestureRecognizer: UIPanGestureRecognizer?
      var originalPosition: CGPoint?
      var currentPositionTouched: CGPoint?
      
      override func viewDidLoad() {
        super.viewDidLoad()
        
        panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
        view.addGestureRecognizer(panGestureRecognizer!)
      }
      
      func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: view)
        
        if panGesture.state == .began {
          originalPosition = view.center
          currentPositionTouched = panGesture.location(in: view)
        } else if panGesture.state == .changed {
            view.frame.origin = CGPoint(
              x: translation.x,
              y: translation.y
            )
        } else if panGesture.state == .ended {
          let velocity = panGesture.velocity(in: view)
    
          if velocity.y >= 1500 {
            UIView.animate(withDuration: 0.2
              , animations: {
                self.view.frame.origin = CGPoint(
                  x: self.view.frame.origin.x,
                  y: self.view.frame.size.height
                )
              }, completion: { (isCompleted) in
                if isCompleted {
                  self.dismiss(animated: false, completion: nil)
                }
            })
          } else {
            UIView.animate(withDuration: 0.2, animations: {
              self.view.center = self.originalPosition!
            })
          }
        }
      }
    }
    
    0 讨论(0)
  • 2020-11-28 00:47

    Swift 4.x, Using Pangesture

    Simple way

    Vertical

    class ViewConrtoller: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrage(_:))))
        }
    
        @objc func onDrage(_ sender:UIPanGestureRecognizer) {
            let percentThreshold:CGFloat = 0.3
            let translation = sender.translation(in: view)
    
            let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
            let progress = progressAlongAxis(newX, view.bounds.width)
    
            view.frame.origin.x = newX //Move view to new position
    
            if sender.state == .ended {
                let velocity = sender.velocity(in: view)
               if velocity.x >= 300 || progress > percentThreshold {
                   self.dismiss(animated: true) //Perform dismiss
               } else {
                   UIView.animate(withDuration: 0.2, animations: {
                       self.view.frame.origin.x = 0 // Revert animation
                   })
              }
           }
    
           sender.setTranslation(.zero, in: view)
        }
    }
    

    Helper function

    func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
            let movementOnAxis = pointOnAxis / axisLength
            let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
            let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
            return CGFloat(positiveMovementOnAxisPercent)
        }
    
        func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T : Comparable {
            return min(max(value, minimum), maximum)
        }
    

    Hard way

    Refer this -> https://github.com/satishVekariya/DraggableViewController

    0 讨论(0)
  • 2020-11-28 00:48

    Only vertical dismiss

    func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: view)
    
        if panGesture.state == .began {
            originalPosition = view.center
            currentPositionTouched = panGesture.location(in: view)    
        } else if panGesture.state == .changed {
            view.frame.origin = CGPoint(
                x:  view.frame.origin.x,
                y:  view.frame.origin.y + translation.y
            )
            panGesture.setTranslation(CGPoint.zero, in: self.view)
        } else if panGesture.state == .ended {
            let velocity = panGesture.velocity(in: view)
            if velocity.y >= 150 {
                UIView.animate(withDuration: 0.2
                    , animations: {
                        self.view.frame.origin = CGPoint(
                            x: self.view.frame.origin.x,
                            y: self.view.frame.size.height
                        )
                }, completion: { (isCompleted) in
                    if isCompleted {
                        self.dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                UIView.animate(withDuration: 0.2, animations: {
                    self.view.center = self.originalPosition!
                })
            }
        }
    
    0 讨论(0)
  • 2020-11-28 00:49

    Here is an extension I made based on @Wilson answer :

    // MARK: IMPORT STATEMENTS
    import UIKit
    
    // MARK: EXTENSION
    extension UIViewController {
    
        // MARK: IS SWIPABLE - FUNCTION
        func isSwipable() {
            let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
            self.view.addGestureRecognizer(panGestureRecognizer)
        }
    
        // MARK: HANDLE PAN GESTURE - FUNCTION
        @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
            let translation = panGesture.translation(in: view)
            let minX = view.frame.width * 0.135
            var originalPosition = CGPoint.zero
    
            if panGesture.state == .began {
                originalPosition = view.center
            } else if panGesture.state == .changed {
                view.frame.origin = CGPoint(x: translation.x, y: 0.0)
    
                if panGesture.location(in: view).x > minX {
                    view.frame.origin = originalPosition
                }
    
                if view.frame.origin.x <= 0.0 {
                    view.frame.origin.x = 0.0
                }
            } else if panGesture.state == .ended {
                if view.frame.origin.x >= view.frame.width * 0.5 {
                    UIView.animate(withDuration: 0.2
                         , animations: {
                            self.view.frame.origin = CGPoint(
                                x: self.view.frame.size.width,
                                y: self.view.frame.origin.y
                            )
                    }, completion: { (isCompleted) in
                        if isCompleted {
                            self.dismiss(animated: false, completion: nil)
                        }
                    })
                } else {
                    UIView.animate(withDuration: 0.2, animations: {
                        self.view.frame.origin = originalPosition
                    })
                }
            }
        }
    
    }
    

    USAGE

    Inside your view controller you want to be swipable :

    override func viewDidLoad() {
        super.viewDidLoad()
    
        self.isSwipable()
    }
    

    and it will be dismissible by swiping from the extreme left side of the view controller, as a navigation controller.

    0 讨论(0)
  • 2020-11-28 00:50

    For those who really wanna dive a little deeper into Custom UIViewController Transition, I recommend this great tutorial from raywenderlich.com.

    The original final sample project contains bug. So I fixed it and upload it to Github repo. The proj is in Swift 5, so you can easily run and play it.

    Here is a preview:

    And it's interactive too!

    Happy hacking!

    0 讨论(0)
  • 2020-11-28 00:54

    Massively updates the repo for Swift 4.

    For Swift 3, I have created the following to present a UIViewController from right to left and dismiss it by pan gesture. I have uploaded this as a GitHub repository.

    DismissOnPanGesture.swift file:

    //  Created by David Seek on 11/21/16.
    //  Copyright © 2016 David Seek. All rights reserved.
    
    import UIKit
    
    class DismissAnimator : NSObject {
    }
    
    extension DismissAnimator : UIViewControllerAnimatedTransitioning {
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.6
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    
            let screenBounds = UIScreen.main.bounds
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            var x:CGFloat      = toVC!.view.bounds.origin.x - screenBounds.width
            let y:CGFloat      = toVC!.view.bounds.origin.y
            let width:CGFloat  = toVC!.view.bounds.width
            let height:CGFloat = toVC!.view.bounds.height
            var frame:CGRect   = CGRect(x: x, y: y, width: width, height: height)
    
            toVC?.view.alpha = 0.2
    
            toVC?.view.frame = frame
            let containerView = transitionContext.containerView
    
            containerView.insertSubview(toVC!.view, belowSubview: fromVC!.view)
    
    
            let bottomLeftCorner = CGPoint(x: screenBounds.width, y: 0)
            let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
    
            UIView.animate(
                withDuration: transitionDuration(using: transitionContext),
                animations: {
                    fromVC!.view.frame = finalFrame
                    toVC?.view.alpha = 1
    
                    x = toVC!.view.bounds.origin.x
                    frame = CGRect(x: x, y: y, width: width, height: height)
    
                    toVC?.view.frame = frame
                },
                completion: { _ in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                }
            )
        }
    }
    
    class Interactor: UIPercentDrivenInteractiveTransition {
        var hasStarted = false
        var shouldFinish = false
    }
    
    let transition: CATransition = CATransition()
    
    func presentVCRightToLeft(_ fromVC: UIViewController, _ toVC: UIViewController) {
        transition.duration = 0.5
        transition.type = kCATransitionPush
        transition.subtype = kCATransitionFromRight
        fromVC.view.window!.layer.add(transition, forKey: kCATransition)
        fromVC.present(toVC, animated: false, completion: nil)
    }
    
    func dismissVCLeftToRight(_ vc: UIViewController) {
        transition.duration = 0.5
        transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        transition.type = kCATransitionPush
        transition.subtype = kCATransitionFromLeft
        vc.view.window!.layer.add(transition, forKey: nil)
        vc.dismiss(animated: false, completion: nil)
    }
    
    func instantiatePanGestureRecognizer(_ vc: UIViewController, _ selector: Selector) {
        var edgeRecognizer: UIScreenEdgePanGestureRecognizer!
        edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: vc, action: selector)
        edgeRecognizer.edges = .left
        vc.view.addGestureRecognizer(edgeRecognizer)
    }
    
    func dismissVCOnPanGesture(_ vc: UIViewController, _ sender: UIScreenEdgePanGestureRecognizer, _ interactor: Interactor) {
        let percentThreshold:CGFloat = 0.3
        let translation = sender.translation(in: vc.view)
        let fingerMovement = translation.x / vc.view.bounds.width
        let rightMovement = fmaxf(Float(fingerMovement), 0.0)
        let rightMovementPercent = fminf(rightMovement, 1.0)
        let progress = CGFloat(rightMovementPercent)
    
        switch sender.state {
        case .began:
            interactor.hasStarted = true
            vc.dismiss(animated: true, completion: nil)
        case .changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.update(progress)
        case .cancelled:
            interactor.hasStarted = false
            interactor.cancel()
        case .ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finish()
                : interactor.cancel()
        default:
            break
        }
    }
    

    Easy usage:

    import UIKit
    
    class VC1: UIViewController, UIViewControllerTransitioningDelegate {
    
        let interactor = Interactor()
    
        @IBAction func present(_ sender: Any) {
            let vc = self.storyboard?.instantiateViewController(withIdentifier: "VC2") as! VC2
            vc.transitioningDelegate = self
            vc.interactor = interactor
    
            presentVCRightToLeft(self, vc)
        }
    
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return DismissAnimator()
        }
    
        func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactor.hasStarted ? interactor : nil
        }
    }
    
    class VC2: UIViewController {
    
        var interactor:Interactor? = nil
    
        override func viewDidLoad() {
            super.viewDidLoad()
            instantiatePanGestureRecognizer(self, #selector(gesture))
        }
    
        @IBAction func dismiss(_ sender: Any) {
            dismissVCLeftToRight(self)
        }
    
        func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
            dismissVCOnPanGesture(self, sender, interactor!)
        }
    }
    
    0 讨论(0)
提交回复
热议问题