Benutzerdefinierte Übergangsanimation des Navigationscontrollers

74

Ich habe einige Tutorials befolgt, um beim Übergang von einer Ansicht zur anderen eine benutzerdefinierte Animation zu erstellen.

Mein Testprojekt mit benutzerdefiniertem Segue von hier aus funktioniert einwandfrei, aber jemand hat mir gesagt, dass es nicht mehr empfohlen wird, benutzerdefinierte Animationen innerhalb eines benutzerdefinierten Segues zu erstellen, und ich sollte verwenden UIViewControllerAnimatedTransitioning.

Ich habe mehrere Tutorials befolgt, die dieses Protokoll verwenden, aber alle handeln von der modalen Präsentation (zum Beispiel dieses Tutorial ).

Was ich versuche, ist ein Push-Segue innerhalb eines Navigationscontrollerbaums, aber wenn ich versuche, dasselbe mit einem Show- (Push-) Segue zu tun, funktioniert es nicht mehr.

Bitte sagen Sie mir, wie Sie in einem Navigationscontroller eine benutzerdefinierte Übergangsanimation von einer Ansicht zur anderen ausführen können.

Und gibt es überhaupt eine Methode für alle Übergangsanimationen? Es wäre umständlich, wenn ich eines Tages die gleiche Animation machen möchte, aber am Ende den Code zweimal duplizieren muss, um am Übergang von Modal zu Controller zu arbeiten.

AVAVT
quelle
@Rob Urgh Es tut mir leid, wenn das idiotisch klingt, aber wie mache ich meinen View Controller zum Delegierten des Navigationscontrollers? Ich kann sie scheinbar nicht an das Storyboard binden und mein 'navigationController: animationControllerForOperation: fromViewController: toViewController:' wird nie aufgerufen.
AVAVT
Sie können nur tun self.navigationController.delegate = self;in viewDidLoad. Möglicherweise erhalten Sie eine Warnung, es sei denn, Sie geben in der @interfaceZeile an, dass Ihr View Controller dem <UINavigationControllerDelegate>Protokoll entspricht .
Rob
Dieser Code ist schnell 4 für den kreisförmigen Übergang. stackoverflow.com/a/49321985/8334818
Pramod More

Antworten:

195

Um einen benutzerdefinierten Übergang mit Navigation Controller ( UINavigationController) durchzuführen, sollten Sie:

  • Definieren Sie Ihren View Controller so, dass er dem UINavigationControllerDelegateProtokoll entspricht. Sie können beispielsweise eine private Klassenerweiterung in der .mDatei Ihres View Controllers haben , die die Konformität mit diesem Protokoll angibt:

    @interface ViewController () <UINavigationControllerDelegate>
    
    @end
    
  • Stellen Sie sicher, dass Sie Ihren View Controller tatsächlich als Delegierten Ihres Navigationscontrollers angeben:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.navigationController.delegate = self;
    }
    
  • Implementieren Sie animationControllerForOperationin Ihrem View Controller:

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                      animationControllerForOperation:(UINavigationControllerOperation)operation
                                                   fromViewController:(UIViewController*)fromVC
                                                     toViewController:(UIViewController*)toVC
    {
        if (operation == UINavigationControllerOperationPush)
            return [[PushAnimator alloc] init];
    
        if (operation == UINavigationControllerOperationPop)
            return [[PopAnimator alloc] init];
    
        return nil;
    }
    
  • Implementieren Sie Animatoren für Push- und Pop-Animationen, z.

    @interface PushAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @interface PopAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @implementation PushAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        return 0.5;
    }
    
    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
        UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
        [[transitionContext containerView] addSubview:toViewController.view];
    
        toViewController.view.alpha = 0.0;
    
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toViewController.view.alpha = 1.0;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
    @end
    
    @implementation PopAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        return 0.5;
    }
    
    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
        UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
        [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
    
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.alpha = 0.0;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
    @end
    

    Das verblasst zwar beim Übergang, aber Sie können die Animation jederzeit nach Belieben anpassen.

  • Wenn Sie interaktive Gesten verarbeiten möchten (z. B. das native Wischen von links nach rechts zum Pop), müssen Sie einen Interaktionscontroller implementieren:

    • Definieren Sie eine Eigenschaft für einen Interaktionscontroller (ein Objekt, das den Anforderungen entspricht UIViewControllerInteractiveTransitioning):

      @property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactionController;
      

      Dies UIPercentDrivenInteractiveTransitionist ein schönes Objekt, das das Aktualisieren Ihrer benutzerdefinierten Animation auf der Grundlage der Vollständigkeit der Geste erleichtert.

    • Fügen Sie Ihrer Ansicht eine Gestenerkennung hinzu. Hier implementiere ich nur den linken Gestenerkenner, um einen Pop zu simulieren:

      UIScreenEdgePanGestureRecognizer *edge = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFromLeftEdge:)];
      edge.edges = UIRectEdgeLeft;
      [view addGestureRecognizer:edge];
      
    • Implementieren Sie den Gestenerkennungs-Handler:

      /** Handle swipe from left edge
       *
       * This is the "action" selector that is called when a left screen edge gesture recognizer starts.
       *
       * This will instantiate a UIPercentDrivenInteractiveTransition when the gesture starts,
       * update it as the gesture is "changed", and will finish and release it when the gesture
       * ends.
       *
       * @param   gesture       The screen edge pan gesture recognizer.
       */
      
      - (void)handleSwipeFromLeftEdge:(UIScreenEdgePanGestureRecognizer *)gesture {
          CGPoint translate = [gesture translationInView:gesture.view];
          CGFloat percent   = translate.x / gesture.view.bounds.size.width;
      
          if (gesture.state == UIGestureRecognizerStateBegan) {
              self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
              [self popViewControllerAnimated:TRUE];
          } else if (gesture.state == UIGestureRecognizerStateChanged) {
              [self.interactionController updateInteractiveTransition:percent];
          } else if (gesture.state == UIGestureRecognizerStateEnded) {
              CGPoint velocity = [gesture velocityInView:gesture.view];
              if (percent > 0.5 || velocity.x > 0) {
                  [self.interactionController finishInteractiveTransition];
              } else {
                  [self.interactionController cancelInteractiveTransition];
              }
              self.interactionController = nil;
          }
      }
      
    • In Ihrem Navigationscontroller-Delegaten müssen Sie auch die interactionControllerForAnimationControllerDelegierungsmethode implementieren

      - (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                               interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
          return self.interactionController;
      }
      

Wenn Sie "UINavigationController Custom Transition Tutorial" googeln, erhalten Sie viele Treffer. Oder sehen Sie sich das Video zu benutzerdefinierten Übergängen von WWDC 2013 an .

rauben
quelle
Wie kann man den Standard-Swipe nach hinten beibehalten (Screen Edge Gesture)?
kidsid49
@ kidsid49 - Wenn Sie benutzerdefinierte Übergänge ausführen, verlieren Sie die integrierte interaktive Pop-Geste, haben aber auch die Möglichkeit, Ihre eigenen Interaktions-Controller zu implementieren. Weitere Informationen finden Sie in diesem WWDC-Video. Lesen Sie die Diskussion über UIPercentDrivenInteractiveTransition(was den Prozess erheblich vereinfacht). Ich habe oben einige relevante Code-Schnipsel hinzugefügt.
Rob
@Rob Vielen Dank, dass Sie eine so detaillierte Antwort gegeben haben. Ich konnte meiner App erfolgreich benutzerdefinierte Animationen hinzufügen. Ich habe jedoch eine Abfrage. Ich habe eine benutzerdefinierte Pop-Animation hinzugefügt, sodass beim Wischen des Benutzers von links nach rechts die Ansicht von links angezeigt wird, genau wie bei einem normalen Pop ohne benutzerdefinierte Animationen. Das Problem ist, dass ich nicht scheinen kann, die Ansicht von links zu bekommen, um genau meinem Daumen / Finger zu folgen. Es gibt immer einen gewissen Abstand zwischen dem Ort, an dem ich mich berühre, und dem Ort, an dem sich die Grenze der linken Ansicht befindet. Wie würde ich das machen? Ist es so, dass die Ansichtsgrenze genau dort ist, wo ich sie berühre?
Shumais Ul Haq
Wie feuern Sie diesen Push / Pop-Übergang ab? Ich habe versucht, Segmente (z. B. Push anzeigen) im Storyboard zu erstellen und die Methode performSegueWithIdentifier von viewControllers auf dem Stapel in NavigationController mit einem Delegaten zu verwenden, der den Übergang wie beschrieben behandelt, aber nicht funktioniert. Wie kann dieser Übergang beginnen?
Marcin Kapusta
1
@Pavan - Sie animationControllerForOperationkönnen einfach das fromVCund / oder überprüfen toVCund zurückkehren, nilwenn Sie nicht möchten, dass eine benutzerdefinierte Animation ausgeführt wird. Am einfachsten ist es, zu überprüfen, ob es sich um eine bestimmte Klasse handelt. Eleganter können Sie ein optionales Protokoll entwerfen, an das sich View-Controller anpassen können, mit einer booleschen Eigenschaft, die angibt, ob sie das benutzerdefinierte Push / Pop möchten oder nicht.
Rob
15

Möglicherweise möchten Sie den folgenden Code bereits hinzufügen addSubview

  toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

Aus einer anderen Frage: Benutzerdefinierter Übergang für Push-Animation mit Navigationscontroller auf iOS-9

Aus der Apple-Dokumentation für finalFrameForViewController:

Gibt das Endrahmenrechteck für die Ansicht des angegebenen Ansichtscontrollers zurück.

Das von dieser Methode zurückgegebene Rechteck repräsentiert die Größe der entsprechenden Ansicht am Ende des Übergangs. Für die Ansicht, die während der Präsentation behandelt wird, kann der von dieser Methode zurückgegebene Wert CGRectZero sein, es kann sich jedoch auch um ein gültiges Rahmenrechteck handeln.

Q i
quelle
4
Wow, das behebt mein Problem. Warum scheint diese Information nirgendwo anders zu sein, wo ich gesucht habe? Wie Apple-Dokumentation, andere Tutorials ... Es macht für mich keinen Sinn, dass Sie dies einstellen müssen.
David
1
Punkt ist, dass ich Autolayout verwende und den Rahmen nirgendwo anders einstelle ...
David
9

Unter Verwendung der perfekten Antworten von Rob & Q i ist hier der vereinfachte Swift-Code, der dieselbe Fade-Animation für .push und .pop verwendet:

extension YourViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        //INFO: use UINavigationControllerOperation.push or UINavigationControllerOperation.pop to detect the 'direction' of the navigation

        class FadeAnimation: NSObject, UIViewControllerAnimatedTransitioning {
            func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
                return 0.5
            }

            func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
                let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
                if let vc = toViewController {
                    transitionContext.finalFrame(for: vc)
                    transitionContext.containerView.addSubview(vc.view)
                    vc.view.alpha = 0.0
                    UIView.animate(withDuration: self.transitionDuration(using: transitionContext),
                    animations: {
                        vc.view.alpha = 1.0
                    },
                    completion: { finished in
                        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                    })
                } else {
                    NSLog("Oops! Something went wrong! 'ToView' controller is nill")
                }
            }
        }

        return FadeAnimation()
    }
}

Vergessen Sie nicht, den Delegaten in der viewDidLoad () -Methode von YourViewController festzulegen:

override func viewDidLoad() {
    //...
    self.navigationController?.delegate = self
    //...
}
Edi
quelle
5

Es funktioniert sowohl schnell 3 als auch 4

@IBAction func NextView(_ sender: UIButton) {
  let newVC = self.storyboard?.instantiateViewControllerWithIdentifier(withIdentifier: "NewVC") as! NewViewController

  let transition = CATransition()
  transition.duration = 0.5
  transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
  transition.type = kCATransitionPush
  transition.subtype = kCAGravityLeft
  //instead "kCAGravityLeft" try with different transition subtypes

  self.navigationController?.view.layer.add(transition, forKey: kCATransition)
  self.navigationController?.pushViewController(newVC, animated: false)
}
Sai Kumar Reddy
quelle