Wie kann man unter iOS nach unten ziehen, um ein Modal zu schließen?


88

Ein üblicher Weg, ein Modal zu schließen, besteht darin, nach unten zu streichen. Wie können wir dem Benutzer erlauben, das Modal nach unten zu ziehen, wenn es weit genug ist, wird das Modal entlassen, andernfalls wird es wieder an die ursprüngliche Position animiert?

Wir finden dies beispielsweise in den Fotoansichten der Twitter-App oder im "Discovery" -Modus von Snapchat.

Ähnliche Threads weisen darauf hin, dass wir einen UISwipeGestureRecognizer und [self EntlassungViewControllerAnimated ...] verwenden können, um eine modale VC zu schließen, wenn ein Benutzer nach unten wischt. Dies behandelt jedoch nur einen einzigen Wisch und lässt den Benutzer das Modal nicht herumziehen.


Sehen Sie sich benutzerdefinierte interaktive Übergänge an. So können Sie es implementieren. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX

Von Robert Chen auf github.com/ThornTechPublic/InteractiveModal Repo verwiesen und eine Wrapper / Handler-Klasse geschrieben, um alles zu handhaben. Kein Boilerplate-Code mehr unterstützt vier grundlegende Übergänge (von oben nach unten, von unten nach oben, von links nach rechts und von rechts nach links) mit abweisenden Gesten. Github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/…
Chamira Fernando

@ChamiraFernando, schaute auf Ihren Code und es hilft sehr. Gibt es eine Möglichkeit, es so zu gestalten, dass mehrere Richtungen anstelle einer enthalten sind?
Jevon Cowell

Ich werde tun. Die Zeit ist heutzutage sehr begrenzt :(
Chamira Fernando

Antworten:


93

Ich habe gerade ein Tutorial erstellt, um ein Modal interaktiv nach unten zu ziehen, um es zu schließen.

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

Ich fand dieses Thema zunächst verwirrend, daher wird es im Tutorial Schritt für Schritt erläutert.

Geben Sie hier die Bildbeschreibung ein

Wenn Sie den Code nur selbst ausführen möchten, ist dies das Repo:

https://github.com/ThornTechPublic/InteractiveModal

Dies ist der Ansatz, den ich verwendet habe:

Controller anzeigen

Sie überschreiben die Entlassungsanimation mit einer benutzerdefinierten. Wenn der Benutzer das Modal zieht, wird es interactoraktiviert.

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

Animator schließen

Sie erstellen einen benutzerdefinierten Animator. Dies ist eine benutzerdefinierte Animation, die Sie in ein UIViewControllerAnimatedTransitioningProtokoll packen.

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

Interaktor

Sie Unterklasse, UIPercentDrivenInteractiveTransitiondamit es als Ihre Zustandsmaschine fungieren kann. Da beide VCs auf das Interaktorobjekt zugreifen, können Sie damit den Schwenkfortschritt verfolgen.

import UIKit

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

Modal View Controller

Dadurch wird der Schwenkgestenstatus Interaktormethodenaufrufen zugeordnet. Der translationInView() yWert bestimmt, ob der Benutzer einen Schwellenwert überschritten hat. Wenn die Schwenkgeste aktiviert ist .Ended, wird der Interaktor entweder beendet oder abgebrochen.

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
Hey Robert, tolle Arbeit. Haben Sie eine Idee, wie wir dies ändern können, damit es mit Tabellenansichten funktioniert? Das heißt, in der Lage zu sein, nach unten zu ziehen, um zu entlassen, wenn die Tabellenansicht oben ist? Vielen Dank
Ross Barbish

1
Ross, ich habe eine neue Niederlassung erstellt, die ein funktionierendes Beispiel enthält: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Wenn Sie zuerst sehen möchten, wie es aussieht, lesen Sie dieses GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . Die Tabellenansicht verfügt über einen integrierten panGestureRecognizer, der über eine Zielaktion mit der vorhandenen handleGesture (_ :) -Methode verbunden werden kann. Um Konflikte mit dem normalen Scrollen von Tabellen zu vermeiden, wird die Pulldown-Entlassung nur aktiviert, wenn die Tabelle nach oben gescrollt wird. Ich habe einen Schnappschuss verwendet und auch viele Kommentare hinzugefügt.
Robert Chen

Robert, mehr großartige Arbeit. Ich habe meine eigene Implementierung erstellt, die die vorhandenen tableView-Schwenkmethoden wie scrollViewDidScroll, scrollViewWillBeginDragging verwendet. Für die tableView müssen sowohl Bounces als auch bouncesVertically auf true gesetzt sein. Auf diese Weise können wir das ContentOffset der Tableview-Elemente messen. Der Vorteil dieser Methode besteht darin, dass die Tabellenansicht scheinbar mit einer Geste vom Bildschirm gewischt werden kann, wenn genügend Geschwindigkeit vorhanden ist (aufgrund des Springens). Ich werde Ihnen wahrscheinlich irgendwann in dieser Woche eine Pull-Anfrage senden. Beide Optionen scheinen gültig zu sein.
Ross Barbish

Gute Arbeit @RossBarbish, ich kann es kaum erwarten zu sehen, wie du das geschafft hast. Es wird ziemlich süß sein, in einer fließenden Bewegung nach oben scrollen und dann in den interaktiven Übergangsmodus wechseln zu können.
Robert Chen

1
Stellen Sie die Präsentationseigenschaft segueauf ein over current context, um einen schwarzen Bildschirm auf der Rückseite zu vermeiden, wenn Sie den viewController herunterziehen
nitish005

61

Ich werde erzählen, wie ich es in Swift 3 gemacht habe:

Ergebnis

Implementierung

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

Wo die Modals View Controller von einem erben class, den ich erstellt habe (ViewControllerPannable ) , um sie bei Erreichen einer bestimmten Geschwindigkeit zu machen.

ViewControllerPannable-Klasse

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
Ich habe Ihren Code kopiert und es hat funktioniert. Bu Hintergrund der Modellansicht, wenn Pulldown ist schwarz, nicht transparent wie Ihre
Nguyễn Anh Việt

5
Im Storyboard aus Attribute Inspector Panel Storyboard seguedes MainViewControllerauf ModalViewController: Stellen Sie die Präsentation Eigenschaft Over Current Context
Wilson

Dies scheint einfacher zu sein als die akzeptierte Antwort, aber ich erhalte einen Fehler in der ViewControllerPannable-Klasse. Der Fehler lautet "Wert des Nichtfunktionstyps UIPanGestureRecognizer kann nicht aufgerufen werden". Es steht in der Zeile "panGestureRecognizer = UIPanGestureRecognizer (Ziel: self, action: #selector (panGestureAction (_ :)))" Irgendwelche Ideen?
tylerSF

Der erwähnte Fehler wurde behoben, indem er in UIPanGestureRecognizer geändert wurde. "panGestureRecognizer = panGestureRecognizer (Ziel ..." geändert in: "panGestureRecognizer = UIPanGestureRecognizer (Ziel ..."
tylerSF

Wie kann ich den schwarzen Hintergrund beim Entlassen entfernen, da ich den VC nicht modal präsentiere?
Mr. Bean

16

Hier ist eine Ein-Datei-Lösung basierend auf der Antwort von @ wilson (danke 👍) mit den folgenden Verbesserungen:


Liste der Verbesserungen gegenüber der vorherigen Lösung

  • Begrenzen Sie das Schwenken so, dass die Ansicht nur nach unten zeigt:
    • Vermeiden Sie horizontale Verschiebungen, indem Sie nur die yKoordinate von aktualisierenview.frame.origin
    • Vermeiden Sie es, beim Wischen mit aus dem Bildschirm zu schwenken let y = max(0, translation.y)
  • Schließen Sie den Ansichts-Controller auch basierend darauf, wo der Finger losgelassen wird (standardmäßig in der unteren Hälfte des Bildschirms) und nicht nur basierend auf der Geschwindigkeit des Wischens
  • Zeigen Sie den Ansichts-Controller als modal an, um sicherzustellen, dass der vorherige Ansichts-Controller dahinter angezeigt wird, und vermeiden Sie einen schwarzen Hintergrund (sollte Ihre Frage @ nguyễn-anh-việt beantworten).
  • Nicht benötigte entfernen currentPositionTouched undoriginalPosition
  • Legen Sie die folgenden Parameter offen:
    • minimumVelocityToHide: Welche Geschwindigkeit reicht aus, um sich zu verstecken (standardmäßig 1500)
    • minimumScreenRatioToHide: Wie niedrig ist genug, um sich zu verstecken (standardmäßig 0,5)
    • animationDuration : Wie schnell verstecken / zeigen wir (standardmäßig 0,2 s)

Lösung

Swift 3 & 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;
    }
}

In Ihrem Code gibt es entweder Tippfehler oder eine fehlende Variable "MinimumHeightRatioToHide"
Shokaveli

1
Danke @shokaveli, behoben (war minimumScreenRatioToHide)
Agirault

Dies ist eine sehr schöne Lösung. Ich habe jedoch ein kleines Problem und bin mir nicht ganz sicher, was die Ursache ist: dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 Der rote Hintergrund ist Teil des modalen VC, der blaue Hintergrund ist Teil des VC präsentierte das Modal. Es gibt dieses unangenehme Verhalten, wenn der Pan-Gesten-Erkenner startet, das ich scheinbar nicht beheben kann.
Knolraap

1
Hallo @Knolraap. Schauen self.view.frame.originSie sich vielleicht den Wert von an, bevor Sie sliceViewVerticallyTodas erste Mal anrufen : Es scheint, dass der Offset, den wir sehen, mit der Höhe der Statusleiste übereinstimmt. Vielleicht ist Ihr ursprünglicher Ursprung also nicht 0?
Agirault

1
Es ist besser, slideViewVerticallyToals verschachtelte Funktion in zu verwenden onPan.
Nik Kov

15

hat eine Demo zum interaktiven Herunterziehen erstellt, um den View Controller wie den Erkennungsmodus von Snapchat zu schließen. Überprüfen Sie diesen Github für ein Beispielprojekt.

Geben Sie hier die Bildbeschreibung ein


2
Großartig, aber es ist wirklich veraltet. Kennt jemand ein anderes Beispielprojekt wie dieses?
Lernende

14

Swift 4.x, mit Pangesture

Einfacher Weg

Vertikale

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

Hilfsfunktion

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

Harter Weg

Siehe hierzu -> https://github.com/satishVekariya/DraggableViewController


1
Ich habe versucht, Ihren Code zu verwenden. Ich möchte jedoch eine geringfügige Änderung vornehmen, wenn sich meine Unteransicht unten befindet. Wenn der Benutzer die Ansicht zieht, sollte sich die Höhe der Unteransicht auch in Bezug auf die getippte Position erhöhen. Hinweis: - das Gestenereignis, das in die Unteransicht gestellt wurde
Ekra

Klar, du kannst es schaffen
SPatel

Hi @SPatel Haben Sie eine Idee, wie Sie diesen Code ändern können, um ihn auf der linken Seite der x-Achse zu ziehen, dh negative Bewegungen entlang der x-Achse?
Nikhil Pandey

1
@SPatel Sie müssen auch die Überschrift der Antwort ändern, da vertikal die Bewegungen der x-Achse und horizontal die Bewegungen der y-Achse anzeigt.
Nikhil Pandey

1
Denken Sie daran, einzustellen modalPresentationStyle = UIModalPresentationOverFullScreen, um den Rückbildschirm hinter dem zu vermeiden view.
tounaobun

13

Ich habe einen super einfachen Weg gefunden, dies zu tun. Geben Sie einfach den folgenden Code in Ihren View Controller ein:

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
Danke, funktioniert perfekt! Legen Sie einfach einen Pan Gesture Recogniser in der Ansicht im Interface Builder ab und stellen Sie eine Verbindung mit der obigen @IBAction her.
Balazs630

Funktioniert auch in Swift 5. Folgen Sie einfach den Anweisungen von @ balazs630.
LondonGuy

Ich denke, das ist der beste Weg.
Enkha

@Alex Shubin, Wie kann ich beim Ziehen von ViewController auf TabbarController schließen?
Vadlapalli Masthan

11

Aktualisiert das Repo für Swift 4 massiv .

Für Swift 3 habe ich Folgendes erstellt, um ein UIViewControllervon rechts nach links darzustellen und es durch eine Schwenkgeste zu schließen. Ich habe dies als GitHub-Repository hochgeladen .

Geben Sie hier die Bildbeschreibung ein

DismissOnPanGesture.swift Datei:

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

Einfache Bedienung:

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
Einfach super! Danke für das Teilen.
Sharad Chauhan

Hallo, wie kann ich mit Pan Gesture Recognizer präsentieren, bitte helfen Sie mir, danke
Muralikrishna

Ich starte gerade einen Youtube-Kanal mit Tutorials. Ich könnte eine Episode erstellen, um dieses Problem für iOS 13+ / Swift 5
David Seek

6

Was Sie beschreiben, ist eine interaktive benutzerdefinierte Übergangsanimation . Sie passen sowohl die Animation als auch die Fahrgeste eines Übergangs an, dh die Entlassung (oder nicht) eines präsentierten Ansichtscontrollers. Die einfachste Möglichkeit zur Implementierung besteht darin, einen UIPanGestureRecognizer mit einer UIPercentDrivenInteractiveTransition zu kombinieren.

In meinem Buch wird erklärt, wie das geht, und ich habe Beispiele (aus dem Buch) veröffentlicht. Dieses spezielle Beispiel ist eine andere Situation - der Übergang erfolgt seitwärts und nicht nach unten, und es handelt sich um einen Tab-Bar-Controller, nicht um einen vorgestellten Controller -, aber die Grundidee ist genau dieselbe:

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

Wenn Sie dieses Projekt herunterladen und ausführen, werden Sie feststellen, dass genau das passiert, was Sie beschreiben, außer dass es seitwärts verläuft: Wenn der Luftwiderstand mehr als die Hälfte beträgt, wechseln wir, aber wenn nicht, brechen wir ab und rasten wieder ein Ort.


3
404 Seite nicht gefunden.
Trapper

6

Nur vertikale Entlassung

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

Ich habe eine benutzerfreundliche Erweiterung erstellt.

Einfach Ihren UIViewController mit InteractiveViewController inhärent und Sie sind fertig InteractiveViewController

Rufen Sie die Methode showInteractive () von Ihrem Controller auf, um sie als interaktiv anzuzeigen.

Geben Sie hier die Bildbeschreibung ein


4

In Ziel C: Hier ist der Code

imviewDidLoad

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

Wie kann man die Zeit für das Wischen nach unten steuern, bevor es entlassen wird?
TonyTony

2

Hier ist eine Erweiterung, die ich basierend auf der @ Wilson-Antwort gemacht habe:

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

}

VERWENDUNG

In Ihrem View Controller möchten Sie wischbar sein:

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

und es kann durch Wischen von der äußersten linken Seite des Ansichts-Controllers als Navigations-Controller verworfen werden.


Hey, ich habe Ihren Code verwendet und er funktioniert perfekt für das richtige Wischen, aber ich möchte ihn beim Wischen nach unten schließen. Wie kann ich das tun? Bitte helfen Sie!
Khush

2

Dies ist meine einfache Klasse für Drag ViewController von der Achse . Nur herited Ihre Klasse von DraggableViewController.

MyCustomClass: DraggableViewController

Arbeiten Sie nur für den präsentierten 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)
    }
}

2

Für diejenigen, die wirklich etwas tiefer in Custom UIViewController Transition eintauchen möchten, empfehle ich dieses großartige Tutorial von raywenderlich.com .

Das ursprüngliche endgültige Beispielprojekt enthält einen Fehler. Also habe ich es repariert und auf Github Repo hochgeladen . Das Projekt befindet sich in Swift 5, sodass Sie es problemlos ausführen und spielen können.

Hier ist eine Vorschau:

Und es ist auch interaktiv!

Viel Spaß beim Hacken!


0

Sie können einen UIPanGestureRecognizer verwenden, um das Ziehen des Benutzers zu erkennen und die modale Ansicht damit zu verschieben. Wenn die Endposition weit genug unten ist, kann die Ansicht verworfen oder auf andere Weise an ihre ursprüngliche Position zurück animiert werden.

In dieser Antwort finden Sie weitere Informationen zur Implementierung von so etwas.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.