Pop up! Transcrit sur iOS

Bonjour, Habr! Tout le monde aime les applications réactives. Encore mieux quand ils ont des animations pertinentes. Dans cet article, je vais dire et montrer avec toute la "viande" comment montrer, cacher, tordre, faire tournoyer et tout faire correctement avec des écrans contextuels.



Au départ, je voulais écrire un article indiquant que sur iOS 10 un UIViewPropertyAnimator pratique UIViewPropertyAnimator apparu qui résout le problème des animations interrompues. Maintenant, ils peuvent être arrêtés, inversés, poursuivis ou annulés. Apple appelle cette interface fluide .

Mais j'ai réalisé: il est difficile de parler d'interrompre l'animation des contrôleurs sans décrire comment ces transitions sont correctement animées. Par conséquent, il y aura deux articles. Dans ce document, nous découvrirons comment afficher et masquer correctement l'écran, ainsi que les interruptions dans le prochain.

Comment fonctionnent-ils


UIViewController a une propriété transitioningDelegate . Il s'agit d'un protocole avec différentes fonctions, chacune retourne un objet:


  • animationController pour l'animation,
  • interactionController pour interrompre les animations,
  • presentationController pour l'affichage: hiérarchie, cadre, etc.


Sur la base de tout cela, nous allons créer un panneau contextuel:



Contrôleurs de cuisson


Vous pouvez animer la transition pour les contrôleurs modaux et pour UINavigationController (fonctionne via UINavigationControllerDelegate ).
Nous considérerons les transitions modales. La configuration du contrôleur avant le spectacle est un peu inhabituelle:


 class ParentViewController: UIViewController { private let transition = PanelTransition() // 1 @IBAction func openDidPress(_ sender: Any) { let child = ChildViewController() child.transitioningDelegate = transition // 2 child.modalPresentationStyle = .custom // 3 present(child, animated: true) } } 

  1. Créez un objet qui décrit la transition. transitioningDelegate marqué comme weak , vous devez donc stocker la transition séparément par strong lien strong .
  2. Nous avons défini notre transition vers transitioningDelegate .
  3. Afin de contrôler la méthode d'affichage dans presentationController vous devez spécifier .custom pour modalPresentationStyle. .

Le contrôleur illustré ne sait pas du tout comment il est affiché. Et c'est bien.


Afficher en demi-écran


Commençons le code pour PanelTransition avec presentationController . Vous l'avez UIPopoverController si vous avez créé des fenêtres contextuelles via UIPopoverController . PresentationController contrôle l'affichage du contrôleur: cadre, hiérarchie, etc. Il décide comment afficher les popovers sur l'iPad: avec quel cadre, de quel côté du bouton afficher, ajoute du flou à l'arrière-plan de la fenêtre et s'assombrit en dessous.



Notre structure est similaire: nous allons assombrir le fond, mettre le cadre pas en plein écran:



Pour commencer, dans la presentationController(forPresented:, presenting:, source:) , retournez la classe PresentationController :


 class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return presentationController = PresentationController(presentedViewController: presented, presenting: presenting ?? source) } 

Pourquoi 3 contrôleurs sont-ils transmis et quelle est la source?

Source est le contrôleur sur lequel nous avons appelé l'animation du spectacle. Mais le contrôleur qui participera à la tranche est le premier de la hiérarchie avec definesPresentationContext = true . Si le contrôleur change, alors le vrai contrôleur indicateur sera dans le paramètre de presenting.


Vous pouvez maintenant implémenter la classe PresentationController . Pour commencer, encadrons le futur contrôleur. Il existe une méthode frameOfPresentedViewInContainerView pour frameOfPresentedViewInContainerView . Laissez le contrôleur occuper la moitié inférieure de l'écran:


 class PresentationController: UIPresentationController { override var frameOfPresentedViewInContainerView: CGRect { let bounds = containerView!.bounds let halfHeight = bounds.height / 2 return CGRect(x: 0, y: halfHeight, width: bounds.width, height: halfHeight) } } 

Vous pouvez démarrer le projet et essayer d'afficher l'écran, mais rien ne se passera. En effet, nous gérons maintenant nous-mêmes la hiérarchie des vues et nous devons ajouter manuellement la vue du contrôleur:


 // PresentationController.swift override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() containerView?.addSubview(presentedView!) } 

Encore faut-il mettre un cadre pour presentedView . containerViewDidLayoutSubviews est le meilleur endroit, car de cette façon, nous pouvons répondre à la rotation de l'écran:


 // PresentationController.swift override func containerViewDidLayoutSubviews() { super.containerViewDidLayoutSubviews() presentedView?.frame = frameOfPresentedViewInContainerView } 

Vous pouvez maintenant courir. L'animation sera standard pour UIModalTransitionStyle.coverVertical , mais le cadre aura la moitié de la taille.


Assombrir l'arrière-plan


La tâche suivante consiste à assombrir le contrôleur d'arrière-plan pour se concentrer sur ce qui est affiché.


Nous hériterons de PresentationController et le remplacerons par une nouvelle classe dans le fichier PanelTransition . Dans la nouvelle classe, il n'y aura qu'un code de gradation.


 class DimmPresentationController: PresentationController 

Créez une vue que nous superposerons sur:


 private lazy var dimmView: UIView = { let view = UIView() view.backgroundColor = UIColor(white: 0, alpha: 0.3) view.alpha = 0 return view }() 

Nous changerons alpha vues alpha conformément à l'animation de transition. Il existe 4 méthodes:


  • presentationTransitionWillBegin
  • presentationTransitionDidEnd
  • dismissalTransitionWillBegin
  • dismissalTransitionDidEnd

Le premier est le plus difficile. Ajoutez dimmView à la hiérarchie, posez le cadre et démarrez l'animation:


 override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() containerView?.insertSubview(dimmView, at: 0) performAlongsideTransitionIfPossible { [unowned self] in self.dimmView.alpha = 1 } } 

L'animation est lancée à l'aide d'une fonction auxiliaire:


 private func performAlongsideTransitionIfPossible(_ block: @escaping () -> Void) { guard let coordinator = self.presentedViewController.transitionCoordinator else { block() return } coordinator.animate(alongsideTransition: { (_) in block() }, completion: nil) } 

Nous avons défini le cadre pour dimmView dans containerViewDidLayoutSubviews (comme la dernière fois):


 override func containerViewDidLayoutSubviews() { super.containerViewDidLayoutSubviews() dimmView.frame = containerView!.frame } 

L'animation peut être interrompue et annulée, et si elle est annulée, alors dimmView doit être supprimé de la hiérarchie:


 override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if !completed { self.dimmView.removeFromSuperview() } } 

Le processus inverse démarre dans les méthodes de masquage. Mais maintenant, vous devez supprimer dimmView uniquement si l'animation est terminée.


 override func dismissalTransitionWillBegin() { super.dismissalTransitionWillBegin() performAlongsideTransitionIfPossible { [unowned self] in self.dimmView.alpha = 0 } } override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { self.dimmView.removeFromSuperview() } } 

Maintenant, le fond s'assombrit.


Nous contrôlons l'animation


Nous montrons le contrôleur par le bas


Nous pouvons maintenant animer l'apparence du contrôleur. Dans la classe PanelTransition , renvoyez la classe qui contrôlera l'animation d'apparence:


 func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PresentAnimation() } 

La mise en œuvre du protocole est simple:


 extension PresentAnimation: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let animator = self.animator(using: transitionContext) animator.startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { return self.animator(using: transitionContext) } } 

Le code clé est un peu plus compliqué:


 class PresentAnimation: NSObject { let duration: TimeInterval = 0.3 private func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { // transitionContext.view    ,   let to = transitionContext.view(forKey: .to)! let finalFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!) //   ,     PresentationController //      to.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) { to.frame = finalFrame //   ,     } animator.addCompletion { (position) in //  ,      transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return animator } } 

UIViewPropertyAnimator ne fonctionne pas dans iOS 9

La solution de contournement est assez simple: vous devez utiliser non pas l'animateur dans le code animateTransition , mais l'ancienne UIView.animate… Par exemple, comme ceci:


 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let to = transitionContext.view(forKey: .to)! let finalFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!) to.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height) UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { to.frame = finalFrame }) { (_) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } }    ,   `interruptibleAnimator(using transitionContext:)` 

Si vous ne rendez pas interruptible, la méthode interruptibleAnimator peut être omise. La discontinuité sera prise en compte dans le prochain article, abonnez-vous.


Cachez le contrôleur vers le bas


Tout est pareil, mais dans la direction opposée. Dans Panel Transition créez la classe DismissAnimation :


 func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return DismissAnimation() } 

Et nous le réalisons. Classe d' DismissAnimation :


 class DismissAnimation: NSObject { let duration: TimeInterval = 0.3 private func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { let from = transitionContext.view(forKey: .from)! let initialFrame = transitionContext.initialFrame(for: transitionContext.viewController(forKey: .from)!) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) { from.frame = initialFrame.offsetBy(dx: 0, dy: initialFrame.height) } animator.addCompletion { (position) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return animator } } extension DismissAnimation: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let animator = self.animator(using: transitionContext) animator.startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { return self.animator(using: transitionContext) } } 

À cet endroit, vous pouvez expérimenter avec les parties:
- un scénario alternatif peut apparaître ci-dessous;
- à droite - navigation rapide dans le menu;
- ci-dessus - message d'information:



Pizza Dodo , Snack et Savey


La prochaine fois, nous ajoutons une fermeture interactive avec un geste, puis nous interrompons son animation. Si vous ne pouvez pas attendre, le projet complet est déjà sur le github.


Et voici que la deuxième partie de l'article est arrivée.

Abonnez-vous à la chaîne Dodo Pizza Mobile.

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


All Articles