web-dev-qa-db-fra.com

Lorsque vous implémentez des présentations de contrôleur de vue personnalisées, où appliquer les contraintes de la vue présentée?

J'ai lu toute la documentation d'Apple sur les présentations modales personnalisées et je n'ai pas trouvé cette réponse. Lors de la présentation d'un contrôleur de vue à l'aide d'une animation personnalisée, nous pouvons renvoyer 3 éléments (si la transition n'est pas interactive; 4 éléments interactifs peuvent en renvoyer): un contrôleur de présentation et deux contrôleurs d'animation (un pour le présent et un pour le renvoi).

Dans le contrôleur de présentation et dans le contrôleur d'animation actuel, aucun code d'Apple n'inclut de contraintes. Apple recommande de définir le cadre de la vue du contrôleur de vue présenté dans le contrôleur d'animation actuel (ci-dessous):

// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;

...et c'est tout.

Le problème que j'ai est que la barre d'état double hauteur n'est pas reconnue par ces contrôleurs de vue présentés dans le simulateur (et les périphériques que je possède). Pour être plus précis, aucune solution unique ne fonctionne dans tous les simulateurs - les solutions qui fonctionnent dans les nouveaux simulateurs (comme iPhone 8) ne fonctionnent pas dans les simulateurs plus anciens (comme iPhone 5). Si je laisse Apple gérer la présentation à l'aide d'une animation UIKit par défaut, la barre d'état double hauteur est gérée correctement par le contrôleur de vue présenté; par conséquent, je peux supposer que les contraintes dans le contrôleur de vue présenté ne sont pas le problème.

Je me suis donc tourné vers les méthodes containerViewDidLayoutSubviews et containerViewWillLayoutSubviews du contrôleur de présentation, sans aucune chance:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.frame = containerView!.bounds
}

Le code ci-dessus ne fonctionne dans le simulateur que la première fois qu'une barre d'état à double hauteur est introduite; à partir de là, cette méthode ne répond plus. Pour vérifier ceci:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.frame = containerView!.bounds
    print("did layout")
}

la méthode ci-dessus cesse d’imprimer sur la console après la première introduction de la barre d’état à double hauteur. Mais si je change la tâche en quelque chose qui ne change pas la taille du cadre de la vue présentée, comme changer sa couleur d'arrière-plan, par exemple:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.backgroundColor = UIColor.blue
    print("did layout")
}

la méthode ne casse jamais. Quelle que soit la raison, modifier la taille du cadre de la vue présentée dans cette méthode annule la méthode après le premier basculement de la barre d'état à double hauteur. J'aimerais une explication de ce comportement. Peu probable un bug, mais semble en être un. 

Quoi qu'il en soit, Apple affirme que si la mise en page automatique est correctement utilisée, le programmeur n'a rien à faire! Donc ma question est, où ou comment sommes-nous censés donner les contraintes de vue du contrôleur de vue présenté afin qu'il puisse s'adapter à son conteneur temporaire? Parce que, IMO, c'est le problème. Le contrôleur de vue présenté appartient à la vue de conteneur de la transition, qui est une vue temporaire fournie par UIKit sur laquelle nous n'avons pas beaucoup de pouvoir. Si nous pouvons ancrer la vue présentée à ce conteneur, tous les problèmes sont résolus. Mais je n'ai jamais vu Apple faire cela ni même en parler.

Remarque: Ancrer explicitement la vue présentée dans la vue conteneur n'importe où: dans le contrôleur d'animation actuel (avant l'animation ou dans son gestionnaire d'achèvement), dans le contrôleur de présentation ou dans une méthode de délégation adaptative, ne produit pas des résultats cohérents. comme mentionné précédemment.

CONCLUSIONS: (1) Il n’existe aucun moyen de gérer officiellement, correctement, proprement ou de manière cohérente une barre de statut à double hauteur avec des présentations modales; (2) Apple l'a bâclée avec la barre d'état double hauteur et ne peut pas attendre le jour où tous les iPhones auront une encoche à l'écran.

15
bsod

Ma réponse est: Vous ne devriez pas utiliser de contraintes dans le cas de présentations modales personnalisées

Je connais donc votre douleur et je vais donc essayer de vous aider à gagner du temps et à épargner des efforts en vous fournissant des indices que j'ai tout à coup révélés. 


Exemple de cas:

Animation de l'interface de la carte comme suit:

 enter image description here

Termes à utiliser ultérieurement:

  • Parent _ - UIViewController avec élément du bouton de barre "Détail"
  • Enfant - UIViewController avec "un autre"

Les problèmes que vous avez mentionnés ont commencé lorsque mon animation a impliqué un changement de taille avec le mouvement. Il provoque différents types d'effets, notamment: 

  • La zone de barre d'état inférieure du parent est apparue et a disparu
  • Les vues secondaires du parent étaient mal animées - sauts, duplications et autres problèmes.

Après quelques jours de débogage et de recherche, j'ai proposé la solution suivante (désolé pour certains nombres magiques;)):

UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0.4,
                       options: .curveEaseIn, animations: {
            toVC.view.transform = CGAffineTransform(translationX: 0, y: self.finalFrame.minY)
            toVC.view.frame = self.finalFrame
            toVC.view.layer.cornerRadius = self.cornerRadius

            fromVC.view.layer.cornerRadius = self.cornerRadius
            var transform = CATransform3DIdentity
            transform = CATransform3DScale(transform, scale, scale, 1.0)
            transform = CATransform3DTranslate(transform, 0, wdiff, 0)
            fromVC.view.layer.transform = transform
            fromVC.view.alpha = 0.6
        }) { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }

Le point principal ici est que Vous devez utiliser CGAffineTransform3D pour éviter les problèmes d’animation et les problèmes d’animation de sous-vues (Transformations 2D ne fonctionnent pas pour des raisons inconnues). 

J'espère que cette approche résoudra tous vos problèmes sans utiliser de contraintes.

Sentez-vous libre de poser des questions.

UPD: selon la barre d'état en cours d'appel

Après des heures de toutes les expériences possibles et de l’examen de projets similaires tels que ceci et ceci et de questions à débordement telles que ceci , ceci (c’est vraiment amusant, la réponse des OP est là) et similaire. suis totalement confus. On dirait que ma solution gère Double barre d'état sur le niveau UIKit (elle s'ajuste correctement), mais le même mouvement ignore les transformations précédentes. La raison est inconnue.


Exemples de code:

Vous pouvez voir la solution de travail ici sur Github

P.S. Je ne suis pas sûr de pouvoir poster un lien GitHub dans la réponse. J'apprécierais pour un conseil comment poster le code de 100-300 lignes dans la réponse. 

5
fewlinesofcode

Je me débattais avec statusBar à double hauteur dans mon projet actuel et j'ai pu résoudre presque tous les problèmes (le dernier problème restant est un problème de transformation très étrange lorsque PresentViewController est intégré dans un UITabBarController).

Lorsque la hauteur de la barre d'état change, une notification est publiée.
Votre sous-classe UIPresentationController devrait s'abonner à cette notification particulière et ajuster le cadre de la containerView et ses sous-vues:

UIApplication.willChangeStatusBarFrameNotification

Voici un exemple de code que j'utilise:

final class MyCustomPresentationController: UIPresentationController {

    // MARK: - StatusBar

    private func subscribeToStatusBarNotifications() {
        let notificationName = UIApplication.willChangeStatusBarFrameNotification
        NotificationCenter.default.addObserver(self, selector: #selector(statusBarWillChangeFrame(notification:)), name: notificationName, object: nil)
    }

    @objc private func statusBarWillChangeFrame(notification: Notification?) {
        if let newFrame = notification?.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? CGRect {
            statusBarWillChangeFrame(to: newFrame)
        } else {
            statusBarWillChangeFrame(to: .zero)
        }
    }

    func statusBarWillChangeFrame(to newFrame: CGRect) {
        layoutContainerView(animated: true)
    }

    // MARK: - Object Lifecycle

    deinit {
        // Unsubscribe from all notifications
        NotificationCenter.default.removeObserver(self)
    }

    // MARK: - Layout

    /// Called when the status-bar is about to change its frame.
    /// Relayout the containerView and its subviews
    private func layoutContainerView(animated: Bool) {
        guard let containerView = self.containerView else { return }

        // Retrieve informations about status-bar
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let normalStatusBarHeight = Constants.Number.statusBarNormalHeight // 20
        let isStatusBarNormal = statusBarHeight ==~ normalStatusBarHeight

        if animated {
            containerView.frame = …
            updatePresentedViewFrame(animated: true)
        } else {
            // Update containerView frame
            containerView.frame = …
            updatePresentedViewFrame(animated: false)
        }
    }

    func updatePresentedViewFrame(animated: Bool) {
        self.presentedView?.frame = …
    }
}

 result image

0
Vin Gazoil