Nicht auftauchen! Unterbrechbare Übergänge in iOS

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


 // PanelTransition.swift func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return driver } 

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() //   percentComplete   0 let isRunning = percentComplete != 0 if !isRunning { presentingController?.present(presentedController!, animated: true) } case .changed: update(percentComplete + r.incrementToBottom(maxTranslation: maxTranslation)) case .ended, .cancelled: if r.isProjectedToDownHalf(maxTranslation: maxTranslation) { finish() } else { cancel() } case .failed: cancel() default: break } } 

.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 { // Velocity value func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat { // Magic formula from WWDC let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000 return self * multiplier } } extension CGPoint { static func +(left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y) } } 

.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() // Pause allows to detect percentComplete if percentComplete == 0 { presentedController?.dismiss(animated: true) } 

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 //    panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handle(recognizer:))) presentedController.view.addGestureRecognizer(panRecognizer!) //    screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePresentation(recognizer:))) screenEdgePanRecognizer!.edges = .bottom presentingController.view.addGestureRecognizer(screenEdgePanRecognizer!) } 

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:


  1. Lassen Sie uns in viewDidLoad einen viewDidLoad Controller viewDidLoad , da wir möglicherweise jederzeit einen Controller benötigen.
  2. Erstellen Sie PanelTransition . In seinem Konstruktor ist die Geste an den Controller gebunden.
  3. Legen Sie das TransitioningDelegate für den untergeordneten Controller ab.
  4. 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() // 1 transition = PanelTransition(presented: child, presenting: self) // 2 // Setup the child child.modalPresentationStyle = .custom child.transitioningDelegate = transition // 3 } override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { return .bottom // 4 } } 

    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.

Source: https://habr.com/ru/post/de465073/


All Articles