Sous iOS, comment faire glisser vers le bas pour ignorer un modal?


90

Une façon courante de rejeter un modal est de faire glisser vers le bas - Comment permet-on à l'utilisateur de faire glisser le modal vers le bas, s'il est assez loin, le modal est ignoré, sinon il s'anime à sa position d'origine?

Par exemple, nous pouvons le trouver utilisé sur les vues de photos de l'application Twitter ou sur le mode «découverte» de Snapchat.

Des fils similaires soulignent que nous pouvons utiliser un UISwipeGestureRecognizer et [self licenciementViewControllerAnimated ...] pour rejeter un VC modal lorsqu'un utilisateur glisse vers le bas. Mais cela ne gère qu'un seul balayage, ne laissant pas l'utilisateur faire glisser le modal.


Jetez un œil aux transitions interactives personnalisées. C'est ainsi que vous pouvez l'implémenter. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX

Référence à github.com/ThornTechPublic/InteractiveModal repo par Robert Chen et a écrit une classe wrapper / handler pour tout gérer. Plus de code standard prend en charge quatre transitions de base (de haut en bas, de bas en haut, de gauche à droite et de droite à gauche) avec des gestes de rejet github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/…
Chamira Fernando

@ChamiraFernando, j'ai regardé votre code et ça aide beaucoup. Existe-t-il un moyen de faire en sorte que plusieurs directions soient incluses au lieu d'une seule?
Jevon Cowell

Je ferai. Le temps est énorme ces jours-ci :(
Chamira Fernando

Réponses:


93

Je viens de créer un tutoriel pour faire glisser interactivement un modal pour le rejeter.

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

J'ai trouvé ce sujet déroutant au début, donc le tutoriel construit cela étape par étape.

entrez la description de l'image ici

Si vous souhaitez simplement exécuter le code vous-même, voici le dépôt:

https://github.com/ThornTechPublic/InteractiveModal

Voici l'approche que j'ai utilisée:

Voir le contrôleur

Vous remplacez l'animation de rejet par une animation personnalisée. Si l'utilisateur fait glisser le modal, le interactorfichier démarre.

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
    }
}

Ignorer l'animateur

Vous créez un animateur personnalisé. Il s'agit d'une animation personnalisée que vous conditionnez dans un UIViewControllerAnimatedTransitioningprotocole.

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())
            }
        )
    }
}

Interacteur

Vous sous-classez UIPercentDrivenInteractiveTransitionafin qu'il puisse agir comme votre machine d'état. Étant donné que les deux VC accèdent à l'objet interacteur, utilisez-le pour suivre la progression du panoramique.

import UIKit

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

Contrôleur de vue modale

Cela mappe l'état du mouvement de panoramique aux appels de méthode d'interacteur. La translationInView() yvaleur détermine si l'utilisateur a franchi un seuil. Lorsque le mouvement de panoramique est effectué .Ended, l'interacteur se termine ou s'annule.

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
        }
    }

}

4
Salut Robert, excellent travail. Avez-vous une idée de la manière dont nous pourrions modifier cela pour lui permettre de fonctionner avec des vues de table? Autrement dit, être capable de tirer vers le bas pour rejeter lorsque la vue de la table est en haut? Merci
Ross Barbish

1
Ross, j'ai créé une nouvelle branche qui a un exemple fonctionnel: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Si vous voulez d'abord voir à quoi il ressemble, consultez ce GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . La vue table a un panGestureRecognizer intégré qui peut être connecté à la méthode handleGesture (_ :) existante via l'action cible. Afin d'éviter tout conflit avec le défilement normal de la table, le rejet de la liste déroulante ne démarre que lorsque la table défile vers le haut. J'ai utilisé un instantané et ajouté de nombreux commentaires.
Robert Chen

Robert, plus beau travail. J'ai fait ma propre implémentation qui utilise les méthodes de panoramique tableView existantes telles que scrollViewDidScroll, scrollViewWillBeginDragging. Cela nécessite que tableView ait des rebonds et des rebondsVertically tous les deux définis sur true - de cette façon, nous pouvons mesurer le ContentOffset des éléments tableview. L'avantage de cette méthode est qu'elle semble permettre de faire glisser la vue de la table hors de l'écran en un seul geste s'il y a suffisamment de vitesse (en raison du rebond). Je vous enverrai probablement une pull request cette semaine, les deux options semblent valides.
Ross Barbish

Bon travail @RossBarbish, j'ai hâte de voir comment vous avez réussi. Ce sera plutôt sympa de pouvoir faire défiler vers le haut, puis entrer en mode de transition interactif, le tout en un seul mouvement fluide.
Robert Chen

1
définir la propriété de présentation seguesur over current contextpour éviter un écran noir à l'arrière lorsque vous abaissez le viewController
nitish005

62

Je vais partager comment je l'ai fait dans Swift 3:

Résultat

la mise en oeuvre

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
  }
}

Où les contrôleurs de vue modaux héritent d'un classque j'ai construit ( ViewControllerPannable) pour les rendre déplaçables et rejetables lorsqu'ils atteignent une certaine vitesse.

ViewControllerPannable Classe

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!
        })
      }
    }
  }
}

1
J'ai copié votre code et cela a fonctionné. L'arrière-plan de la vue du modèle lorsque le menu déroulant est noir, n'est pas transparent comme le vôtre
Nguyễn Anh Việt

5
Dans le storyboard du panneau de l' inspecteur d'attributsStoryboard segue de MainViewControllersur ModalViewController: définissez la propriété Presentation sur Over Current Context
Wilson

Cela semble plus facile que la réponse acceptée, mais j'obtiens une erreur dans la classe ViewControllerPannable. L'erreur est "Impossible d'appeler la valeur du type non fonctionnel UIPanGestureRecognizer". C'est sur la ligne "panGestureRecognizer = UIPanGestureRecognizer (target: self, action: #selector (panGestureAction (_ :)))" Des idées?
tylerSF

Correction de l'erreur que j'ai mentionnée en la remplaçant par UIPanGestureRecognizer. "panGestureRecognizer = panGestureRecognizer (target ..." remplacé par: "panGestureRecognizer = UIPanGestureRecognizer (target ..."
tylerSF

Puisque je présente le VC sans le présenter de manière modale, comment puis-je supprimer le fond noir tout en le rejetant?
Mr.Bean

18

Voici une solution à un fichier basée sur la réponse de @ wilson (merci 👍) avec les améliorations suivantes:


Liste des améliorations de la solution précédente

  • Limitez le panoramique pour que la vue ne descende que:
    • Évitez la translation horizontale en ne mettant à jour que les ycoordonnées deview.frame.origin
    • Évitez de vous déplacer hors de l'écran lorsque vous faites glisser let y = max(0, translation.y)
  • Ignorez également le contrôleur de vue en fonction de l'endroit où le doigt est relâché (par défaut dans la moitié inférieure de l'écran) et pas uniquement en fonction de la vitesse du balayage
  • Afficher le contrôleur de vue comme modal pour vous assurer que le contrôleur de vue précédent apparaît derrière et éviter un arrière-plan noir (devrait répondre à votre question @ nguyễn-anh-việt)
  • Retirer inutiles currentPositionTouchedetoriginalPosition
  • Exposez les paramètres suivants:
    • minimumVelocityToHide: quelle vitesse est suffisante pour se cacher (par défaut à 1500)
    • minimumScreenRatioToHide: à quel point est-il assez bas pour se cacher (0,5 par défaut)
    • animationDuration : à quelle vitesse masquons-nous / montrons-nous (par défaut à 0,2 s)

Solution

Swift 3 et Swift 4:

//
//  PannableViewController.swift
//

import UIKit

class PannableViewController: UIViewController {
    public var minimumVelocityToHide: CGFloat = 1500
    public var minimumScreenRatioToHide: CGFloat = 0.5
    public var animationDuration: TimeInterval = 0.2

    override func viewDidLoad() {
        super.viewDidLoad()

        // Listen for pan gesture
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc func onPan(_ panGesture: UIPanGestureRecognizer) {

        func slideViewVerticallyTo(_ y: CGFloat) {
            self.view.frame.origin = CGPoint(x: 0, y: y)
        }

        switch panGesture.state {

        case .began, .changed:
            // If pan started or is ongoing then
            // slide the view to follow the finger
            let translation = panGesture.translation(in: view)
            let y = max(0, translation.y)
            slideViewVerticallyTo(y)

        case .ended:
            // If pan ended, decide it we should close or reset the view
            // based on the final position and the speed of the gesture
            let translation = panGesture.translation(in: view)
            let velocity = panGesture.velocity(in: view)
            let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
                          (velocity.y > minimumVelocityToHide)

            if closing {
                UIView.animate(withDuration: animationDuration, animations: {
                    // If closing, animate to the bottom of the view
                    self.slideViewVerticallyTo(self.view.frame.size.height)
                }, completion: { (isCompleted) in
                    if isCompleted {
                        // Dismiss the view when it dissapeared
                        dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                // If not closing, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    slideViewVerticallyTo(0)
                })
            }

        default:
            // If gesture state is undefined, reset the view to the top
            UIView.animate(withDuration: animationDuration, animations: {
                slideViewVerticallyTo(0)
            })

        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }
}

Dans votre code, il y a une faute de frappe ou une variable manquante "minimumHeightRatioToHide"
shokaveli

1
Merci @shokaveli, corrigé (était minimumScreenRatioToHide)
agirault le

C'est une très belle solution. Cependant, j'ai un petit problème et je ne sais pas trop quelle en est la cause: dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 Le fond rouge fait partie du VC modal, le fond bleu fait partie du VC qui a présenté le modal. Il y a ce comportement glitch lorsque le module de reconnaissance de mouvement de panoramique démarre que je n'arrive pas à résoudre.
Knolraap

1
Salut @Knolraap. Regardez peut-être la valeur de self.view.frame.originavant d'appeler sliceViewVerticallyTola première fois: il semble que le décalage que nous voyons soit le même que la hauteur de la barre d'état, alors peut-être que votre origine initiale n'est pas 0?
agirault

1
Il est préférable de l'utiliser slideViewVerticallyTocomme fonction imbriquée dans onPan.
Nik Kov

16

a créé une démo pour faire glisser de manière interactive vers le bas pour ignorer le contrôleur de vue comme le mode découverte de Snapchat. Vérifiez ce github pour un exemple de projet.

entrez la description de l'image ici


2
Génial, mais c'est vraiment dépassé. Quelqu'un connaît-il un autre exemple de projet comme celui-ci?
thelearner

14

Swift 4.x, Utilisation de Pangesture

Manière simple

Verticale

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

Fonction d'assistance

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

Façon difficile

Référez-vous ceci -> https://github.com/satishVekariya/DraggableViewController


1
J'ai essayé d'utiliser votre code. mais je veux faire un léger changement si ma sous-vue est en bas et lorsque l'utilisateur fait glisser la vue, la hauteur de la sous-vue doit également augmenter par rapport à la position tapée. Remarque: - l'événement de geste il a été mis sur sous
Ekra

Bien sûr, vous pouvez le faire
SPatel

Salut @SPatel Une idée sur la façon de modifier ce code à utiliser pour faire glisser sur le côté gauche de l'axe x, c'est-à-dire des mouvements négatifs le long de l'axe x?
Nikhil Pandey

1
@SPatel Vous devez également changer le cap de la réponse car la verticale montre les mouvements de l'axe x et l'horizontale montre les mouvements de l'axe y.
Nikhil Pandey

1
N'oubliez pas de régler modalPresentationStyle = UIModalPresentationOverFullScreenpour éviter l'écran arrière derrière le view.
tounaobun le

13

J'ai trouvé un moyen très simple de le faire. Mettez simplement le code suivant dans votre contrôleur de vue:

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
    }
}

1
Merci, fonctionne parfaitement! Déposez simplement un outil de reconnaissance de mouvement panoramique dans la vue du générateur d'interface et connectez-vous à @IBAction ci-dessus.
balazs630

Fonctionne également dans Swift 5. Suivez simplement les instructions données par @ balazs630.
LondonGuy

Je pense que c'est la meilleure façon.
Enkha

@Alex Shubin, Comment ignorer lorsque vous faites glisser de ViewController vers TabbarController?
Vadlapalli Masthan

11

Met à jour massivement le repo pour Swift 4 .

Pour Swift 3 , j'ai créé ce qui suit pour présenter un UIViewControllerde droite à gauche et le rejeter par un mouvement de panoramique. J'ai téléchargé ceci en tant que référentiel GitHub .

entrez la description de l'image ici

DismissOnPanGesture.swift fichier:

//  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
    }
}

Utilisation facile:

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!)
    }
}

1
Tout simplement génial! Merci d'avoir partagé.
Sharad Chauhan

Salut, Comment puis-je présenter en utilisant Pan Gesture Recognizer s'il vous plaît aidez-moi merci
Muralikrishna

Je démarre une chaîne YouTube avec des tutoriels en ce moment. Je pourrais créer un épisode pour couvrir ce problème pour iOS 13+ / Swift 5
David Seek

6

Ce que vous décrivez est une animation de transition personnalisée et interactive . Vous personnalisez à la fois l'animation et le geste de pilotage d'une transition, c'est-à-dire le rejet (ou non) d'un contrôleur de vue présenté. Le moyen le plus simple de l'implémenter consiste à combiner un UIPanGestureRecognizer avec un UIPercentDrivenInteractiveTransition.

Mon livre explique comment faire cela, et j'ai publié des exemples (tirés du livre). Cet exemple particulier est une situation différente - la transition est sur le côté, pas vers le bas, et c'est pour un contrôleur de barre d'onglets, pas un contrôleur présenté - mais l'idée de base est exactement la même:

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

Si vous téléchargez ce projet et que vous l'exécutez, vous verrez que ce qui se passe est exactement ce que vous décrivez, sauf que c'est de côté: si le glissement est supérieur à la moitié, nous effectuons une transition, mais sinon, nous annulons et nous nous remettons endroit.


3
404 Page non trouvée.
trappeur

6

Seul rejet vertical

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!
            })
        }
    }

6

J'ai créé une extension facile à utiliser.

Juste inhérent à votre UIViewController avec InteractiveViewController et vous avez terminé InteractiveViewController

appelez la méthode showInteractive () depuis votre contrôleur pour l'afficher comme Interactive.

entrez la description de l'image ici


4

En Objectif C: voici le code

dansviewDidLoad

UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
                                             initWithTarget:self action:@selector(swipeDown:)];
swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[self.view addGestureRecognizer:swipeRecognizer];

//Swipe Down Method

- (void)swipeDown:(UIGestureRecognizer *)sender{
[self dismissViewControllerAnimated:YES completion:nil];
}

comment contrôler le temps de balayage vers le bas avant qu'il ne soit rejeté?
TonyTony

2

Voici une extension que j'ai créée sur la base de la réponse @Wilson:

// 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

À l'intérieur de votre contrôleur de vue, vous voulez pouvoir faire glisser:

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

et il sera rejeté en faisant glisser depuis le côté extrême gauche du contrôleur de vue, en tant que contrôleur de navigation.


Hé, j'ai utilisé votre code et cela fonctionne parfaitement pour un balayage à droite, mais je veux ignorer le balayage vers le bas, comment puis-je faire cela? Veuillez aider!
Khush

2

C'est ma classe simple pour faire glisser ViewController à partir de l'axe . Juste hérité de votre classe de DraggableViewController.

MyCustomClass: DraggableViewController

Fonctionne uniquement pour ViewController présenté.

// 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)
    }
}

2

Pour ceux qui veulent vraiment plonger un peu plus dans la transition personnalisée UIViewController, je recommande cet excellent tutoriel de raywenderlich.com .

Le projet d'exemple final d'origine contient un bogue. Je l'ai donc corrigé et l'ai téléchargé dans le dépôt Github . Le projet est dans Swift 5, vous pouvez donc l'exécuter et le jouer facilement.

Voici un aperçu:

Et c'est aussi interactif!

Bon piratage!


0

Vous pouvez utiliser un UIPanGestureRecognizer pour détecter le glissement de l'utilisateur et déplacer la vue modale avec lui. Si la position de fin est suffisamment basse, la vue peut être supprimée ou animée à sa position d'origine.

Consultez cette réponse pour plus d'informations sur la mise en œuvre de quelque chose comme celui-ci.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.