web-dev-qa-db-fra.com

Contraintes brisées UIStackViews imbriquées

J'ai une hiérarchie de vues complexe, construite dans Interface Builder, avec UIStackViews imbriqué. J'obtiens des avis de «contraintes insatisfiables» chaque fois que je cache certaines de mes vues de pile internes. Je l'ai trouvé à ceci:

(
    "<NSLayoutConstraint:0x1396632d0 'UISV-canvas-connection' UIStackView:0x1392c5020.top == UILabel:0x13960cd30'Also available on iBooks'.top>",
    "<NSLayoutConstraint:0x139663470 'UISV-canvas-connection' V:[UIButton:0x139554f80]-(0)-|   (Names: '|':UIStackView:0x1392c5020 )>",
    "<NSLayoutConstraint:0x139552350 'UISV-hiding' V:[UIStackView:0x1392c5020(0)]>",
    "<NSLayoutConstraint:0x139663890 'UISV-spacing' V:[UILabel:0x13960cd30'Also available on iBooks']-(8)-[UIButton:0x139554f80]>"
)

Plus précisément, la contrainte UISV-spacing: lorsque vous masquez UIStackView, sa contrainte haute obtient une constante 0, mais cela semble entrer en conflit avec la contrainte d'espacement de la pile interne: il faut 8 points entre mon étiquette et le bouton, ce qui est inconciliable avec la contrainte de masquage crash de contraintes.

Y a-t-il un moyen de contourner ceci? J'ai essayé de masquer de manière récursive toutes les vues de pile internes de la vue de pile masquée, mais cela aboutit à des animations étranges dans lesquelles le contenu flotte hors de l'écran et provoque le démarrage de pertes importantes d'images par le processeur, sans résoudre le problème.

23
Alex Popov

Idéalement, nous pourrions simplement définir la priorité de la contrainte UISV-spacing sur une valeur inférieure, mais cela ne semble pas être possible. :)

Je réussis à définir la propriété spacing des vues de la pile imbriquée sur 0 avant de la masquer, puis de restaurer la valeur appropriée après sa nouvelle visualisation.

Je pense que cela fonctionnerait de manière récursive sur les vues de pile imbriquées. Vous pouvez stocker la valeur d'origine de la propriété spacing dans un dictionnaire et la restaurer ultérieurement.

Mon projet ne comporte qu'un seul niveau d'imbrication. Je ne sais donc pas si cela entraînerait des problèmes de FPS. Tant que vous n'animez pas les changements d'espacement, je ne pense pas que cela créerait trop de succès.

13
Cory Juhlin

Il s'agit d'un problème connu lié au masquage des vues de pile imbriquées.

Il y a essentiellement 3 solutions à ce problème:

  1. Définissez l'espacement sur 0, mais vous devrez alors vous rappeler la valeur précédente.
  2. Appelez innerStackView.removeFromSuperview(), mais vous devrez alors vous rappeler où insérer la vue de pile.
  3. Enveloppez la vue de pile dans UIView avec au moins une contrainte de 999. Par exemple. top @ 1000, menant @ 1000, suivi @ 1000, bas @ 999.

La 3ème option est la meilleure à mon avis. Pour plus d'informations sur ce problème, pourquoi il se produit, sur les différentes solutions et sur la façon de mettre en œuvre la solution 3, voir ma réponse à une question similaire .

17
Senseful

J'ai rencontré un problème similaire avec la dissimulation d'UISV. Pour moi, la solution consistait à réduire les priorités de mes propres contraintes de Required (1000) à quelque chose de moins que cela. Lorsque des contraintes de masquage UISV sont ajoutées, elles sont prioritaires et les contraintes n'entrent plus en conflit.

16
Jaanus

Donc, vous avez ceci:

 broken animation

Et le problème est que, lorsque vous réduisez pour la première fois la pile interne, vous obtenez des erreurs de mise en page automatique:

2017-07-02 15:40:02.377297-0500 nestedStackViews[17331:1727436] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x62800008ce90 'UISV-canvas-connection' UIStackView:0x7fa57a70fce0.top == UILabel:0x7fa57a70ffb0'Top Label of Inner Stack'.top   (active)>",
    "<NSLayoutConstraint:0x62800008cf30 'UISV-canvas-connection' V:[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']-(0)-|   (active, names: '|':UIStackView:0x7fa57a70fce0 )>",
    "<NSLayoutConstraint:0x62000008bc70 'UISV-hiding' UIStackView:0x7fa57a70fce0.height == 0   (active)>",
    "<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x62800008cf80 'UISV-spacing' V:[UILabel:0x7fa57a70ffb0'Top Label of Inner Stack']-(8)-[UILabel:0x7fa57d30def0'Bottom Label of Inner Sta...']   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

Le problème, comme vous l'avez noté, est que la vue de pile externe applique une contrainte height = 0 à la vue de pile intérieure. Cela est en conflit avec la contrainte de remplissage à 8 points appliquée par la vue de pile interne entre ses propres sous-vues. Les deux contraintes ne peuvent pas être satisfaites simultanément.

Je pense que la vue de pile externe utilise cette contrainte height = 0, car elle est plus esthétique lorsqu'elle est animée que de laisser la vue interne masquée sans rétrécissement préalable.

Il existe un correctif simple à cela: encapsuler la vue de la pile interne dans un UIView simple et masquer cet encapsuleur. Je vais démontrer.

Voici le plan de la scène pour la version cassée ci-dessus:

 broken outline

Pour résoudre le problème, sélectionnez la vue de la pile interne. Dans la barre de menus, choisissez Editeur> Intégrer dans> Afficher:

 embed in view

Lorsque j'ai fait cela, Interface Builder a créé une contrainte de largeur sur la vue wrapper. Supprimez cette contrainte de largeur:

 delete width constraint

Ensuite, créez des contraintes entre les quatre bords de l’encapsuleur et de la vue de pile interne:

 create constraints

À ce stade, la présentation est en fait correcte au moment de l'exécution, mais Interface Builder ne la dessine pas correctement. Vous pouvez résoudre ce problème en définissant plus haut les priorités de calage vertical des enfants de la pile interne. Je les ai mis à 800:

 hugging priorities

Nous n'avons pas encore résolu le problème de contrainte insatisfiable. Pour ce faire, recherchez la contrainte inférieure que vous venez de créer et définissez sa priorité sur une valeur inférieure à celle requise. Changeons le en 800:

 change bottom constraint priority

Enfin, vous avez probablement eu une sortie dans votre contrôleur de vue connectée à la vue de pile interne, car vous changiez sa propriété hidden. Modifiez cette prise pour vous connecter à la vue wrapper au lieu de la vue de pile interne. Si le type de votre point de vente est UIStackView, vous devrez le changer en UIView. Le mien était déjà de type UIView, je l'ai donc reconnecté dans le storyboard:

 change outlet

Désormais, lorsque vous basculez la propriété hidden de la vue wrapper, la vue de pile semblera s'effondrer, sans avertissements de contrainte non satisfaisants. Il semble pratiquement identique, je ne vais donc pas m'embêter à publier un autre fichier GIF de l'application en cours d'exécution.

Vous pouvez trouver mon projet test dans ce dépôt github .

15
rob mayoff

Voici l'implémentation de la suggestion n ° 3 de Senseful écrite sous la forme d'une classe Swift 3 utilisant des contraintes SnapKit. J'ai également essayé de modifier les propriétés, mais je ne l'ai jamais fait fonctionner sans avertissements. Je vais donc rester avec UIStackView:

class NestableStackView: UIView {
    private var actualStackView = UIStackView()

    override init(frame: CGRect) {
        super.init(frame: frame);
        addSubview(actualStackView);
        actualStackView.snp.makeConstraints { (make) in
            // Lower edges priority to allow hiding when spacing > 0
            make.edges.equalToSuperview().priority(999);
        }
    }

    convenience init() {
        self.init(frame: CGRect.zero);
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addArrangedSubview(_ view: UIView) {
        actualStackView.addArrangedSubview(view);
    }

    func removeArrangedSubview(_ view: UIView) {
        actualStackView.removeArrangedSubview(view);
    }

    var axis: UILayoutConstraintAxis {
        get {
            return actualStackView.axis;
        }
        set {
            actualStackView.axis = newValue;
        }
    }

    open var distribution: UIStackViewDistribution {
        get {
            return actualStackView.distribution;
        }
        set {
            actualStackView.distribution = newValue;
        }
    }

    var alignment: UIStackViewAlignment {
        get {
            return actualStackView.alignment;
        }
        set {
            actualStackView.alignment = newValue;
        }
    }

    var spacing: CGFloat {
        get {
            return actualStackView.spacing;
        }
        set {
            actualStackView.spacing = newValue;
        }
    }
}
0
Antti

Une autre approche

Essayez d'éviter les UIStackViews imbriqués. Je les aime et construis presque tout avec eux. Mais comme j'ai reconnu qu'ils ajoutaient secrètement des contraintes, j'essaie de ne les utiliser qu'au plus haut niveau et non imbriquées si possible. De cette façon, je peux spécifier la 2e priorité la plus élevée .defaultHigh à la contrainte d'espacement qui résout mes avertissements.

Cette priorité est juste suffisante pour éviter la plupart des problèmes de mise en page. 

Bien sûr, vous devez spécifier quelques contraintes supplémentaires, mais vous pouvez ainsi les contrôler et rendre votre disposition de vue explicite.

0
blackjacx