web-dev-qa-db-fra.com

AutoLayout avec UIViews caché?

Selon moi, il est assez courant de montrer/cacher UIViews, le plus souvent UILabels, en fonction de la logique métier. Ma question est la suivante: quel est le meilleur moyen d’utiliser AutoLayout pour répondre aux vues masquées comme si leur cadre était 0x0. Voici un exemple d'une liste dynamique de 1 à 3 fonctionnalités.

Dynamic features list

À l'heure actuelle, j'ai un espace supérieur de 10 pixels entre le bouton et la dernière étiquette, qui ne glisse évidemment pas lorsque l'étiquette est masquée. À partir de maintenant, j'ai créé un exutoire à cette contrainte et modifié la constante en fonction du nombre d'étiquettes que j'affiche. Ceci est évidemment un peu hacky depuis que j'utilise des valeurs constantes négatives pour pousser le bouton vers le haut sur les images cachées. C’est également mauvais parce qu’il n’est pas limité aux seuls éléments de mise en page, mais à des calculs statiques sournois fondés sur les hauteurs/marges connues d’autres éléments, et qui va bien sûr à l’encontre de la raison pour laquelle AutoLayout a été conçu.

Je pourrais évidemment simplement créer de nouvelles contraintes en fonction de mes libellés dynamiques, mais il s’agit de beaucoup de micro-gestion et de beaucoup de verbosité pour essayer de simplement réduire certains espaces. Y a-t-il de meilleures approches? Changer la taille du cadre 0,0 et laisser AutoLayout agir sans manipuler les contraintes? Supprimer complètement les vues?

Honnêtement cependant, la modification de la constante du contexte de la vue masquée nécessite une seule ligne de code avec un calcul simple. Recréer de nouvelles contraintes avec constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: semble si lourd.

Edit Feb 2018: Voir la réponse de Ben avec UIStackViews

158
Ryan Romanchuk

UIStackView est probablement la solution pour iOS 9+. Non seulement il gère la vue masquée, mais il supprimera également l’espacement et les marges supplémentaires s’il est configuré correctement.

79
Ben Packard

Ma préférence personnelle pour afficher/masquer des vues est de créer un IBOutlet avec la contrainte de largeur ou de hauteur appropriée.

Je mets ensuite à jour la valeur constant en 0 à masquer, ou quelle que soit la valeur à afficher.

Le gros avantage de cette technique est que les contraintes relatives seront maintenues. Par exemple, supposons que vous ayez la vue A et la vue B avec un espace horizontal de x. Lorsque la vue A largeur constant est définie sur 0.f, la vue B se déplace vers la gauche pour remplir cet espace.

Il n'est pas nécessaire d'ajouter ou de supprimer des contraintes, ce qui est une opération très lourde. Le simple fait de mettre à jour la constant de la contrainte fera l'affaire.

231
Max MacLeod

La solution consistant à utiliser une constante 0 lorsqu'elle est masquée et une autre constante si vous la réaffiche est fonctionnelle, mais elle n'est pas satisfaisante si votre contenu a une taille flexible. Vous devez mesurer votre contenu flexible et définir une constante. Cela semble faux et pose des problèmes si la taille du contenu change en raison d'événements liés au serveur ou à l'interface utilisateur.

J'ai une meilleure solution.

L'idée est de définir la règle de hauteur 0 pour qu'elle ait une priorité élevée lorsque nous masquons l'élément afin qu'il ne prenne pas d'espace autolayout.

Voici comment vous faites cela:

1. Définissez une largeur (ou une hauteur) de 0 dans le constructeur d’interface avec une priorité basse.

setting width 0 rule

Interface Builder ne criera pas au sujet des conflits car la priorité est faible. Testez le comportement de hauteur en définissant la priorité sur 999 temporairement (il est interdit de modifier 1000 par programmation, nous ne l'utiliserons donc pas). Le constructeur d'interface va probablement maintenant crier à propos de contraintes conflictuelles. Vous pouvez les résoudre en définissant des priorités sur 900 ou plus pour les objets associés.

2. Ajoutez une sortie pour pouvoir modifier la priorité de la contrainte de largeur dans le code:

outlet connection

. Ajustez la priorité lorsque vous masquez votre élément:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
95
SimplGy

Dans ce cas, je mappe la hauteur de l'étiquette Author sur un IBOutlet approprié:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

et lorsque je règle la hauteur de la contrainte sur 0.0f, nous conservons le "remplissage", car la hauteur du bouton de lecture le permet.

cell.authorLabelHeight.constant = 0; //Hide 
cell.authorLabelHeight.constant = 44; //Show

enter image description here

11
Mitul Marsoniya

Sous-classer la vue et remplacer func intrinsicContentSize() -> CGSize. Il suffit de retourner CGSizeZero si la vue est masquée.

5
Daniel

Je viens de découvrir que pour qu'un UILabel ne prenne pas de place, vous devez le cacher ET définir son texte sur une chaîne vide. (iOS 9)

Le fait de connaître ce fait/bug pourrait aider certaines personnes à simplifier leur présentation, voire même celle de la question d'origine, alors je me suis dit que je la posterais.

4
Matt Koala

Je suis surpris qu'il n'y ait pas d'approche plus élégante fournie par UIKit pour ce comportement souhaité. Il semble très courant de vouloir pouvoir faire.

Étant donné que la connexion des contraintes à IBOutlets et la définition de leurs constantes sur 0 étaient dégoûtantes (et provoquaient NSLayoutConstraint avertissements lorsque votre vue comportait des sous-vues), j'ai décidé de créer une extension qui donne un approche simple et dynamique pour masquer/afficher une UIView ayant des contraintes de mise en forme automatique

Il cache simplement la vue et supprime les contraintes extérieures. Lorsque vous réaffichez la vue, les contraintes sont rajoutées. Le seul inconvénient est que vous devrez spécifier des contraintes de basculement flexibles pour les vues environnantes.

Modifier Cette réponse est destinée à iOS 8.4 et aux versions antérieures. Sous iOS 9, utilisez simplement l'approche UIStackView.

4
Albert Bori

Il y a beaucoup de solutions ici mais mon approche habituelle est encore différente :)

Configurez deux ensembles de contraintes similaires à ceux de Jorge Arimany et TMin:

enter image description here

Les trois contraintes marquées ont la même valeur pour la constante. La priorité définie pour les contraintes A1 et A2 est définie sur 500, tandis que la priorité marquée sur B a pour priorité 250 (ou UILayoutProperty.defaultLow dans le code).

Reliez la contrainte B à un IBOutlet. Ensuite, lorsque vous masquez l'élément, il vous suffit de définir la priorité de contrainte sur haute (750):

constraintB.priority = .defaultHigh

Mais lorsque l'élément est visible, définissez la priorité sur faible (250):

constraintB.priority = .defaultLow

L'avantage (certes mineur) de cette approche par rapport à la simple modification de isActive pour la contrainte B est que vous avez toujours une contrainte de travail si l'élément transitoire est supprimé de la vue par d'autres moyens.

4
SeanR

Je construis la catégorie pour mettre à jour les contraintes facilement:

[myView1 hideByHeight:YES];

Répondez ici: Masquer l'autolayout UIView: Comment faire pour que NSLayoutConstraint existant mette à jour celui-ci

enter image description here

3
Damien Romito

La meilleure pratique consiste à ajouter une hauteur ou une contrainte avec les contraintes de présentation correctes, en fonction de la manière dont vous souhaitez déplacer les vues environnantes et de connecter la contrainte à une propriété IBOutlet.

Assurez-vous que vos propriétés sont strong

dans le code, il suffit de mettre la constante à 0 et activer le, cacher le contenu, ou désactiver = le pour afficher le contenu. C’est mieux que de jouer avec la valeur constante d’une sauvegarde pour la restaurer. N'oubliez pas d'appeler layoutIfNeeded après.

Si le contenu à masquer est groupé, la meilleure pratique consiste à tout mettre dans une vue et à ajouter les contraintes à cette vue.

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
    self.myContainerHeight.active = NO;
    self.myContainer.hidden = NO;
    [self.view layoutIfNeeded];
}
-(void) hideContainer
{
    self.myContainerHeight.active = YES;
    self.myContainerHeight.constant = 0.0f;
    self.myContainer.hidden = YES;
    [self.view layoutIfNeeded];
}

Une fois votre configuration configurée, vous pouvez la tester dans IntefaceBuilder en définissant votre contrainte sur 0, puis sur la valeur d'origine. N'oubliez pas de vérifier les priorités des autres contraintes afin que, lorsqu'elles sont masquées, il n'y ait aucun conflit. Une autre façon de le tester est de le mettre à 0 et de définir la priorité sur 0, mais vous ne devez pas oublier de le restaurer à nouveau avec la priorité la plus élevée.

3
Jorge Arimany

Ma méthode préférée est très similaire à celle suggérée par Jorge Arimany.

Je préfère créer plusieurs contraintes. Commencez par créer vos contraintes lorsque la deuxième étiquette est visible. Créez un point de vente pour la contrainte entre le bouton et la 2e étiquette (si vous utilisez objc, assurez-vous qu’il est solide). Cette contrainte détermine la hauteur entre le bouton et la deuxième étiquette lorsqu'elle est visible.

Créez ensuite une autre contrainte qui spécifie la hauteur entre le bouton et l'étiquette du haut lorsque le deuxième bouton est masqué. Créez un point de vente pour la deuxième contrainte et assurez-vous que ce point de vente a un pointeur fort. Ensuite, décochez la case installed dans le générateur d'interface et assurez-vous que la priorité de la première contrainte est inférieure à cette priorité.

Enfin, lorsque vous masquez la deuxième étiquette, activez la propriété .isActive de ces contraintes et appelez setNeedsDisplay().

Et c'est tout, pas de chiffres magiques, pas de calculs, si vous avez plusieurs contraintes à activer et à désactiver, vous pouvez même utiliser des collections de points de vente pour les conserver organisées par état. (AKA conserve toutes les contraintes cachées dans une OutletCollection et les contraintes non cachées dans une autre et itère simplement sur chaque collection en basculant leur statut .isActive).

Je sais que Ryan Romanchuk a dit qu’il ne voulait pas utiliser plusieurs contraintes, mais j’ai l’impression que cela n’est pas de la microgestion, c’est plus simple que de créer dynamiquement des vues et des contraintes par programme (c’est ce que je pense qu’il voulait éviter si 'ai bien lu la question).

J'ai créé un exemple simple, j'espère que c'est utile ...

import UIKit

class ViewController: UIViewController {

    @IBOutlet var ToBeHiddenLabel: UILabel!


    @IBOutlet var hiddenConstraint: NSLayoutConstraint!
    @IBOutlet var notHiddenConstraint: NSLayoutConstraint!


    @IBAction func HideMiddleButton(_ sender: Any) {

        ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
        notHiddenConstraint.isActive = !notHiddenConstraint.isActive
        hiddenConstraint.isActive = !hiddenConstraint.isActive

        self.view.setNeedsDisplay()
    }
}

enter image description here

2
TMin

Je proposerai aussi ma solution pour offrir de la variété.) Je pense que créer un point de vente pour la largeur/hauteur de chaque élément, ainsi que pour l’espacement, est simplement ridicule et fait exploser le code, les erreurs possibles et le nombre de complications.

Ma méthode supprime toutes les vues (dans mon cas, les instances de UIImageView), sélectionne celles qui doivent être rajoutées et, dans une boucle, elle les rajoute et crée de nouvelles contraintes. C'est en fait très simple, veuillez suivre. Voici mon code rapide et sale pour le faire:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];

// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];

// optionally add the Twitter image
if (self.question.sharedOnTwitter.boolValue) {
    [items addObject:self.twitterImageView];
}

// optionally add the location image
if (self.question.isLocal) {
    [items addObject:self.localQuestionImageView];
}

UIView *previousItem;
UIView *currentItem;

previousItem = items[0];
[self.contentView addSubview:previousItem];

// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
    previousItem = items[i - 1];
    currentItem = items[i];

    [self.contentView addSubview:currentItem];

    [currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(previousItem.mas_centerY);
        make.right.equalTo(previousItem.mas_left).offset(-5);
    }];
}


// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;

[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(previousItem.mas_left);
    make.leading.equalTo(self.text.mas_leading);
    make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

J'obtiens une mise en page et un espacement propres et cohérents. Mon code utilise la maçonnerie, je le recommande vivement: https://github.com/SnapKit/Masonry

0
Zoltán