Sind Sie verrückt nach Popups in Anwendungen? In diesem Artikel werde ich zeigen, wie man Popups interaktiv ausblendet und anzeigt, Animationen unterbrechbar macht und meine Kunden nicht wütend macht.

In einem früheren Artikel habe ich untersucht, wie Sie die Anzeige eines neuen Controllers animieren können.
Wir haben uns für die Tatsache entschieden, dass viewController
animiert viewController
und ausblenden kann:

Jetzt werden wir ihn lehren, auf die Geste der Verschleierung zu reagieren.
Interaktiver Übergang
Fügen Sie eine enge Geste hinzu
Um dem Controller das interaktive Schließen beizubringen, müssen Sie eine Geste hinzufügen und verarbeiten. Alle Arbeiten werden in der TransitionDriver
Klasse ausgeführt:
class TransitionDriver: UIPercentDrivenInteractiveTransition { func link(to controller: UIViewController) { presentedController = controller panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController?.view.addGestureRecognizer(panRecognizer!) } private var presentedController: UIViewController? private var panRecognizer: UIPanGestureRecognizer? }
Sie können einen Handler an der Position des DimmPresentationControllers in PanelTransition anhängen:
private let driver = TransitionDriver() func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { driver.link(to: presented) let presentationController = DimmPresentationController(presentedViewController: presented, presenting: presenting) return presentationController }
Gleichzeitig müssen Sie angeben, dass das Fell verwaltbar geworden ist (dies haben wir bereits im letzten Artikel getan):
Behandle die Geste
Beginnen wir mit der Schließgeste: Wenn Sie das Bedienfeld nach unten ziehen, beginnt die Schließanimation und die Bewegung des Fingers wirkt sich auf den Grad des Schließens aus.
UIPercentDrivenInteractiveTransition
können UIPercentDrivenInteractiveTransition
die Übergangsanimation erfassen und manuell steuern. Es verfügt über Methoden zum update
, finish
und cancel
. Es ist praktisch, die Gestenverarbeitung in der Unterklasse durchzuführen.
Gestenverarbeitung
private func handleDismiss(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause()
.begin
Starten Sie die Entlassung auf die üblichste Weise. Wir haben den Link zum Controller in der Methode link(to:)
gespeichert
.changed
Zählen Sie das Inkrement und übergeben Sie es an die update
. Der akzeptierte Wert kann von 0 bis 1 variieren, daher steuern wir den Abschlussgrad der Animation über die interactionControllerForDismissal(using:)
Methode interactionControllerForDismissal(using:)
Methode interactionControllerForDismissal(using:)
. In der Erweiterung der Geste wurden Berechnungen durchgeführt, damit der Code sauberer wird.
Gestenberechnungen private extension UIPanGestureRecognizer { func incrementToBottom(maxTranslation: CGFloat) -> CGFloat { let translation = self.translation(in: view).y setTranslation(.zero, in: nil) let percentIncrement = translation / maxTranslation return percentIncrement } }
Die Berechnungen basieren auf maxTranslation
, wir berechnen es als die Höhe des angezeigten Controllers:
var maxTranslation: CGFloat { return presentedController?.view.frame.height ?? 0 }
.end
Wir betrachten die Vollständigkeit der Geste. Abschlussregel: Wenn sich mehr als die Hälfte verschoben hat, schließen Sie. In diesem Fall muss der Versatz nicht nur durch die aktuelle Koordinate, sondern auch durch die velocity
berücksichtigt werden. Wir verstehen also die Absicht des Benutzers: Er wird möglicherweise nicht bis zur Mitte fertig, sondern wischt sehr viel nach unten. Oder umgekehrt: abnehmen, aber nach oben wischen, um zurückzukehren.
ProjectedLocation-Berechnungen private extension UIPanGestureRecognizer { func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool { let endLocation = projectedLocation(decelerationRate: .fast) let isPresentationCompleted = endLocation.y > maxTranslation / 2 return isPresentationCompleted } func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal) let projectedLocation = location(in: view!) + velocityOffset return projectedLocation } } extension CGPoint { func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint { return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate), y: y.projectedOffset(decelerationRate: decelerationRate)) } } extension CGFloat {
.cancelled
- wird passieren, wenn Sie den Telefonbildschirm sperren oder wenn sie anrufen. Sie können es als .ended
Block behandeln oder eine Aktion abbrechen.
.failed
- tritt auf, wenn die Geste durch eine andere Geste abgebrochen wird. So kann beispielsweise eine Ziehgeste eine Tippgeste abbrechen.
.possible
- Der Ausgangszustand der Geste erfordert normalerweise nicht viel Arbeit.
Jetzt kann das Panel auch mit einem Wisch geschlossen werden, aber der dismiss
ist dismiss
. Dies geschah, weil in TransitionDriver
eine wantsInteractiveStart
Eigenschaft vorhanden ist. Standardmäßig ist dies der true
. Dies ist normal für einen Schlag, blockiert jedoch die übliche dismiss
.
Betrachten Sie das Verhalten basierend auf dem Status der Geste. Wenn die Geste gestartet wurde, ist dies ein interaktiver Abschluss, und wenn sie nicht gestartet wurde, dann die übliche:
override var wantsInteractiveStart: Bool { get { let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } set { } }
Jetzt kann der Benutzer das Verstecken steuern:

Übergang unterbrechen
Angenommen, wir haben angefangen, unsere Karte zu schließen, haben aber unsere Meinung geändert und möchten zurückkehren. Es ist ganz einfach: In einem .began
Zustand rufen .began
pause()
auf, um anzuhalten.
Sie müssen jedoch zwei Szenarien trennen:
- wenn wir uns vor der Geste verstecken;
- wenn wir den aktuellen unterbrechen.
Überprüfen percentComplete:
dazu nach dem Stoppen percentComplete:
Wenn es 0 ist, schließen wir die Karte manuell und müssen dismiss
abrufen. Wenn es nicht 0 ist, hat das Verstecken bereits begonnen, es reicht aus, nur die Animation zu stoppen:
case .began: pause()
Ich drücke die Taste und wische sofort nach oben, um das Ausblenden aufzuheben:

Beenden Sie die Anzeige des Controllers
Die umgekehrte Situation: Die Karte erschien, aber wir brauchen sie nicht. Wir fangen es und schicken es zurück. In den gleichen Schritten können Sie die Animation der Controller-Anzeige unterbrechen.
Geben Sie den Treiber als interaktiven Display-Controller zurück:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver }
Verarbeiten Sie die Geste, jedoch mit umgekehrten Vorurteilen und Vollständigkeitswerten:
private func handlePresentation(recognizer r: UIPanGestureRecognizer) { switch r.state { case .began: pause() case .changed: let increment = -r.incrementToBottom(maxTranslation: maxTranslation) update(percentComplete + increment) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { cancel() } else { finish() } case .failed: cancel() default: break } }
Um das Ein- und Ausblenden zu trennen, habe ich eine Aufzählung mit der aktuellen Animationsrichtung eingegeben:
enum TransitionDirection { case present, dismiss }
Die Eigenschaft wird in TransitionDriver
gespeichert und wirkt sich darauf aus, welcher Gestenhandler verwendet wird:
var direction: TransitionDirection = .present @objc private func handle(recognizer r: UIPanGestureRecognizer) { switch direction { case .present: handlePresentation(recognizer: r) case .dismiss: handleDismiss(recognizer: r) } }
wantsInteractiveStart
wirkt sich auch auf wantsInteractiveStart
. Wir planen nicht, den Controller mit einer Geste .present
, daher geben wir false
für .present
:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: return false case .dismiss: let gestureIsActive = panRecognizer?.state == .began return gestureIsActive } } set { } }
Nun, es bleibt noch die Richtung der Geste zu ändern, wenn der Controller vollständig gezeigt wurde. Der beste Ort ist in PresentationController
:
override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if completed { driver.direction = .dismiss } }
Ist es ohne Aufzählung möglich?Es scheint, dass wir uns auf die Eigenschaften des Controllers verlassen können: isBeingPresented
und isBeingDismissed
. Aber sie zeigen nur den Prozess, und wir brauchen auch mögliche Anweisungen: Zu Beginn des interaktiven Schließens sind beide Werte false
, und wir müssen bereits wissen, dass dies die Richtung zum Schließen ist. Dies kann durch zusätzliche Bedingungen zum Überprüfen der Hierarchie der Steuerungen gelöst werden, aber die explizite Zuweisung über enum
scheint eine einfachere Lösung zu sein.
Jetzt können Sie die Animation der Show unterbrechen. Ich drücke den Knopf und wische sofort nach unten:

Mit einer Geste anzeigen
Wenn Sie ein Hamburger-Menü für eine Anwendung erstellen, möchten Sie es höchstwahrscheinlich per Geste anzeigen. Dies funktioniert genauso wie interaktives Verstecken, aber in einer Geste, anstatt zu dismiss
nennen dismiss
present
.
Beginnen wir am Ende. handlePresentation(recognizer:)
in handlePresentation(recognizer:)
den Controller an:
case .began: pause() let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) }
Lassen Sie uns interaktiv zeigen:
override var wantsInteractiveStart: Bool { get { switch direction { case .present: let gestureIsActive = screenEdgePanRecognizer?.state == .began return gestureIsActive case .dismiss: … }
Damit der Code funktioniert, gibt es nicht genügend Links zu presentingController
und presentedController
. Wir werden sie beim Erstellen der Geste übergeben und den UIScreenEdgePanGestureRecognizer
hinzufügen:
func linkPresentationGesture(to presentedController: UIViewController, presentingController: UIViewController) { self.presentedController = presentedController self.presentingController = presentingController
Sie können Controller beim Erstellen von PanelTransition
:
class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { init(presented: UIViewController, presenting: UIViewController) { driver.linkPresentationGesture(to: presented, presentingController: presenting) } private let driver = TransitionDriver() }
Es bleibt, um die PanelTransition
zu erstellen:
- Lassen Sie uns in
viewDidLoad
einen viewDidLoad
Controller viewDidLoad
, da wir möglicherweise jederzeit einen Controller benötigen. - Erstellen Sie
PanelTransition
. In seinem Konstruktor ist die Geste an den Controller gebunden. - Legen Sie das TransitioningDelegate für den untergeordneten Controller ab.
Zu Schulungszwecken wische ich von unten, dies steht jedoch im Widerspruch zum Schließen der Anwendung auf dem iPhone X und dem Kontrollzentrum. Durch preferredScreenEdgesDeferringSystemGestures
Verwendung von PreferredScreenEdgesDeferringSystemGestures wurde das Wischen von unten deaktiviert.
class ParentViewController: UIViewController { private var child: ChildViewController! private var transition: PanelTransition! override func viewDidLoad() { super.viewDidLoad() child = ChildViewController()
Nach der Änderung stellte sich heraus, dass ein Problem aufgetreten war: Nach dem ersten Schließen des Panels bleibt es für immer im Status TransitionDirection.dismiss
. Stellen Sie den richtigen Status ein, nachdem Sie den Controller im PresentationController
:
override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { driver.direction = .present } }
Interaktiver Anzeigecode kann in einem separaten Thread angezeigt werden . Es sieht so aus:

Fazit
Infolgedessen können wir den Controller mit unterbrochener Animation anzeigen, und der Benutzer hat die Kontrolle darüber, was auf dem Bildschirm geschieht. Dies ist viel schöner, da die Animation die Benutzeroberfläche nicht mehr blockiert, sondern abgebrochen oder sogar beschleunigt werden kann.
Ein Beispiel ist auf Github zu sehen .
Abonnieren Sie den Dodo Pizza Mobile-Kanal.