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

后端 未结 15 2240
粉色の甜心
粉色の甜心 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:57

    This my simple class for Drag ViewController from axis. Just herited your class from DraggableViewController.

    MyCustomClass: DraggableViewController
    

    Work only for presented ViewController.

    // MARK: - DraggableViewController
    
    public class DraggableViewController: UIViewController {
    
        public let percentThresholdDismiss: CGFloat = 0.3
        public var velocityDismiss: CGFloat = 300
        public var axis: NSLayoutConstraint.Axis = .horizontal
        public var backgroundDismissColor: UIColor = .black {
            didSet {
                navigationController?.view.backgroundColor = backgroundDismissColor
            }
        }
    
        // MARK: LifeCycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrag(_:))))
        }
    
        // MARK: Private methods
    
        @objc fileprivate func onDrag(_ sender: UIPanGestureRecognizer) {
    
            let translation = sender.translation(in: view)
    
            // Movement indication index
            let movementOnAxis: CGFloat
    
            // Move view to new position
            switch axis {
            case .vertical:
                let newY = min(max(view.frame.minY + translation.y, 0), view.frame.maxY)
                movementOnAxis = newY / view.bounds.height
                view.frame.origin.y = newY
    
            case .horizontal:
                let newX = min(max(view.frame.minX + translation.x, 0), view.frame.maxX)
                movementOnAxis = newX / view.bounds.width
                view.frame.origin.x = newX
            }
    
            let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
            let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
            let progress = CGFloat(positiveMovementOnAxisPercent)
            navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(1 - progress)
    
            switch sender.state {
            case .ended where sender.velocity(in: view).y >= velocityDismiss || progress > percentThresholdDismiss:
                // After animate, user made the conditions to leave
                UIView.animate(withDuration: 0.2, animations: {
                    switch self.axis {
                    case .vertical:
                        self.view.frame.origin.y = self.view.bounds.height
    
                    case .horizontal:
                        self.view.frame.origin.x = self.view.bounds.width
                    }
                    self.navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(0)
    
                }, completion: { finish in
                    self.dismiss(animated: true) //Perform dismiss
                })
            case .ended:
                // Revert animation
                UIView.animate(withDuration: 0.2, animations: {
                    switch self.axis {
                    case .vertical:
                        self.view.frame.origin.y = 0
    
                    case .horizontal:
                        self.view.frame.origin.x = 0
                    }
                })
            default:
                break
            }
            sender.setTranslation(.zero, in: view)
        }
    }
    
    0 讨论(0)
  • 2020-11-28 01:04

    I figured out super simple way to do this. Just put the following code into your view controller:

    Swift 4

    override func viewDidLoad() {
        super.viewDidLoad()
        let gestureRecognizer = UIPanGestureRecognizer(target: self,
                                                       action: #selector(panGestureRecognizerHandler(_:)))
        view.addGestureRecognizer(gestureRecognizer)
    }
    
    @IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
        let touchPoint = sender.location(in: view?.window)
        var initialTouchPoint = CGPoint.zero
    
        switch sender.state {
        case .began:
            initialTouchPoint = touchPoint
        case .changed:
            if touchPoint.y > initialTouchPoint.y {
                view.frame.origin.y = touchPoint.y - initialTouchPoint.y
            }
        case .ended, .cancelled:
            if touchPoint.y - initialTouchPoint.y > 200 {
                dismiss(animated: true, completion: nil)
            } else {
                UIView.animate(withDuration: 0.2, animations: {
                    self.view.frame = CGRect(x: 0,
                                             y: 0,
                                             width: self.view.frame.size.width,
                                             height: self.view.frame.size.height)
                })
            }
        case .failed, .possible:
            break
        }
    }
    
    0 讨论(0)
  • 2020-11-28 01:05

    Here is a one-file solution based on @wilson's answer (thanks

    0 讨论(0)
  • 2020-11-28 01:05

    What you're describing is an interactive custom transition animation. You are customizing both the animation and the driving gesture of a transition, i.e. the dismissal (or not) of a presented view controller. The easiest way to implement it is by combining a UIPanGestureRecognizer with a UIPercentDrivenInteractiveTransition.

    My book explains how to do this, and I have posted examples (from the book). This particular example is a different situation - the transition is sideways, not down, and it is for a tab bar controller, not a presented controller - but the basic idea is exactly the same:

    https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p296customAnimation2/ch19p620customAnimation1/AppDelegate.swift

    If you download that project and run it, you will see that what is happening is exactly what you are describing, except that it is sideways: if the drag is more than half, we transition, but if not, we cancel and snap back into place.

    0 讨论(0)
  • 2020-11-28 01:05

    You can use a UIPanGestureRecognizer to detect the user's drag and move the modal view with it. If the ending position is far enough down, the view can be dismissed, or otherwise animated back to its original position.

    Check out this answer for more information on how to implement something like this.

    0 讨论(0)
  • 2020-11-28 01:06

    I just created a tutorial for interactively dragging down a modal to dismiss it.

    http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

    I found this topic to be confusing at first, so the tutorial builds this out step-by-step.

    If you just want to run the code yourself, this is the repo:

    https://github.com/ThornTechPublic/InteractiveModal

    This is the approach I used:

    View Controller

    You override the dismiss animation with a custom one. If the user is dragging the modal, the interactor kicks in.

    import UIKit
    
    class ViewController: UIViewController {
        let interactor = Interactor()
        override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
            if let destinationViewController = segue.destinationViewController as? ModalViewController {
                destinationViewController.transitioningDelegate = self
                destinationViewController.interactor = interactor
            }
        }
    }
    
    extension ViewController: UIViewControllerTransitioningDelegate {
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return DismissAnimator()
        }
        func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactor.hasStarted ? interactor : nil
        }
    }
    

    Dismiss Animator

    You create a custom animator. This is a custom animation that you package inside a UIViewControllerAnimatedTransitioning protocol.

    import UIKit
    
    class DismissAnimator : NSObject {
    }
    
    extension DismissAnimator : UIViewControllerAnimatedTransitioning {
        func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
            return 0.6
        }
    
        func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
            guard
                let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
                let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
                let containerView = transitionContext.containerView()
                else {
                    return
            }
            containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
            let screenBounds = UIScreen.mainScreen().bounds
            let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
            let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
    
            UIView.animateWithDuration(
                transitionDuration(transitionContext),
                animations: {
                    fromVC.view.frame = finalFrame
                },
                completion: { _ in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
                }
            )
        }
    }
    

    Interactor

    You subclass UIPercentDrivenInteractiveTransition so that it can act as your state machine. Since the interactor object is accessed by both VCs, use it to keep track of the panning progress.

    import UIKit
    
    class Interactor: UIPercentDrivenInteractiveTransition {
        var hasStarted = false
        var shouldFinish = false
    }
    

    Modal View Controller

    This maps the pan gesture state to interactor method calls. The translationInView() y value determines whether the user crossed a threshold. When the pan gesture is .Ended, the interactor either finishes or cancels.

    import UIKit
    
    class ModalViewController: UIViewController {
    
        var interactor:Interactor? = nil
    
        @IBAction func close(sender: UIButton) {
            dismissViewControllerAnimated(true, completion: nil)
        }
    
        @IBAction func handleGesture(sender: UIPanGestureRecognizer) {
            let percentThreshold:CGFloat = 0.3
    
            // convert y-position to downward pull progress (percentage)
            let translation = sender.translationInView(view)
            let verticalMovement = translation.y / view.bounds.height
            let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
            let downwardMovementPercent = fminf(downwardMovement, 1.0)
            let progress = CGFloat(downwardMovementPercent)
            guard let interactor = interactor else { return }
    
            switch sender.state {
            case .Began:
                interactor.hasStarted = true
                dismissViewControllerAnimated(true, completion: nil)
            case .Changed:
                interactor.shouldFinish = progress > percentThreshold
                interactor.updateInteractiveTransition(progress)
            case .Cancelled:
                interactor.hasStarted = false
                interactor.cancelInteractiveTransition()
            case .Ended:
                interactor.hasStarted = false
                interactor.shouldFinish
                    ? interactor.finishInteractiveTransition()
                    : interactor.cancelInteractiveTransition()
            default:
                break
            }
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题