Mise en page automatique avec vues UIV cachées?

164

J'ai l'impression que c'est un paradigme assez courant de montrer / cacher UIViews, le plus souvent UILabels, en fonction de la logique métier. Ma question est de savoir quelle est la meilleure façon d'utiliser AutoLayout pour répondre aux vues cachées comme si leur cadre était 0x0. Voici un exemple de liste dynamique de 1 à 3 fonctionnalités.

Liste des fonctionnalités dynamiques

À l'heure actuelle, j'ai un espace supérieur de 10 pixels entre le bouton et la dernière étiquette, qui ne glissera évidemment pas vers le haut lorsque l'étiquette est masquée. À partir de maintenant, j'ai créé une sortie pour cette contrainte et en modifiant la constante en fonction du nombre d'étiquettes que j'affiche. C'est évidemment un peu piraté car j'utilise des valeurs constantes négatives pour pousser le bouton vers le haut sur les cadres cachés. C'est également mauvais car il n'est pas limité aux éléments de mise en page réels, juste des calculs statiques sournois basés sur des hauteurs / rembourrages connus d'autres éléments, et en luttant évidemment contre ce pour quoi AutoLayout a été conçu.

Je pourrais évidemment simplement créer de nouvelles contraintes en fonction de mes étiquettes dynamiques, mais c'est beaucoup de microgestion et beaucoup de verbosité pour essayer de simplement réduire certains espaces. Existe-t-il de meilleures approches? Changer la taille de l'image 0,0 et laisser AutoLayout faire son travail sans manipulation des contraintes? Supprimer complètement les vues?

Honnêtement, la simple modification de la constante à partir 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 Février 2018 : Voir la réponse de Ben avec UIStackViews

Ryan Romanchuk
la source
Merci Ryan pour cette question. J'étais fou de ce qu'il fallait faire pour les cas, comme vous l'avez demandé. Chaque fois que je consulte le didacticiel pour la mise en page automatique, la plupart d'entre eux disent se référer au site du didacticiel raywenderlich que je trouve un peu difficile à comprendre.
Nassif

Réponses:

82

UIStackViewest probablement la voie à suivre pour iOS 9+. Non seulement il gère la vue masquée, mais il supprime également l'espacement et les marges supplémentaires s'il est configuré correctement.

Ben Packard
la source
8
Faites attention lorsque vous utilisez UIStackView dans un UITableViewCell. Pour moi, une fois que la vue est devenue très compliquée, le défilement a commencé à devenir saccadé où il bégayait à chaque fois qu'une cellule faisait défiler vers l'intérieur / l'extérieur. Sous les couvertures, StackView ajoute / supprime des contraintes et cela ne sera pas nécessairement assez efficace pour un défilement fluide.
Kento
7
Ce serait bien de donner un petit exemple de la façon dont la vue de la pile gère cela.
lostintranslation
@Kento est-ce toujours un problème sur iOS 10?
Crashalot
3
Cinq ans plus tard ... dois-je mettre à jour et définir ceci comme réponse acceptée? Il est maintenant disponible depuis trois cycles de publication.
Ryan Romanchuk
1
@RyanRomanchuk Vous devriez, c'est certainement la meilleure réponse maintenant. Merci Ben!
Duncan Luk
234

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

Je mets ensuite à jour la constantvaleur à 0cacher, ou quelle que soit la valeur à afficher.

Le gros avantage de cette technique est que les contraintes relatives seront maintenues. Par exemple, disons que vous avez la vue A et la vue B avec un écart horizontal de x . Lorsque la largeur de la vue A constantest définie sur, la 0.fvue 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 lourde. La simple mise à jour des contraintes constantfera l'affaire.

Max MacLeod
la source
1
exactement. Tant que - dans cet exemple - vous positionnez la vue B à droite de la vue A en utilisant un espace horizontal. J'utilise cette technique tout le temps et cela fonctionne très bien
Max MacLeod
9
@MaxMacLeod Juste pour être sûr: si l'écart entre les deux vues est x, pour que la vue B commence à partir de la position de la vue A, nous devons également changer la constante de contrainte d'écart à 0, non?
kernix
1
@kernix oui s'il devait y avoir une contrainte d'espace horizontal entre les deux vues. la constante 0 signifie bien sûr aucun écart. Ainsi, masquer la vue A en définissant sa largeur sur 0 déplacerait la vue B vers la gauche pour qu'elle soit alignée avec le conteneur. Btw merci pour les votes!
Max MacLeod
2
@MaxMacLeod Merci pour votre réponse. J'ai essayé de faire cela avec un UIView qui contient d'autres vues, mais les sous-vues de cette vue ne sont pas masquées lorsque je règle sa contrainte de hauteur sur 0. Cette solution est-elle censée fonctionner avec des vues contenant des sous-vues? Merci!
shaunlim
6
J'apprécierais également une solution qui fonctionne pour un UIView contenant d'autres vues ...
Mark Gibaud
96

La solution d'utiliser une constante 0lorsqu'elle est masquée et une autre constante si vous la montrez à nouveau est fonctionnelle, mais elle n'est pas satisfaisante si votre contenu a une taille flexible. Vous auriez besoin de mesurer votre contenu flexible et de définir un recul constant. Cela semble faux et pose des problèmes si le contenu change de taille en raison d'événements du serveur ou de l'interface utilisateur.

J'ai une meilleure solution.

L'idée est de définir la règle de hauteur a 0 pour avoir une priorité élevée lorsque nous masquons l'élément afin qu'il ne prenne aucun espace de mise en page automatique.

Voici comment procéder:

1. définissez une largeur (ou hauteur) de 0 dans le générateur d'interface avec une faible priorité.

définition de la règle de largeur 0

Interface Builder ne crie pas sur les conflits car la priorité est faible. Testez le comportement de la hauteur en définissant temporairement la priorité sur 999 (1000 est interdit de muter par programme, nous ne l'utilisons donc pas). Le constructeur d'interface va probablement crier maintenant sur les contraintes conflictuelles. Vous pouvez résoudre ces problèmes en définissant des priorités sur les objets associés à environ 900.

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

connexion de sortie

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

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999
SimplGy
la source
@SimplGy pouvez-vous s'il vous plaît me dire quel est cet endroit.closingSoon?
Niharika
Cela ne fait pas partie de la solution générale, @Niharika, c'est juste ce qu'était la règle commerciale dans mon cas (montrez la vue si le restaurant ferme / ne ferme pas bientôt).
SimplGy
11

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

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

et quand je règle la hauteur de la contrainte à 0.0f, nous conservons le "padding", car la hauteur du bouton Play le permet.

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

entrez la description de l'image ici

Mitul Marsoniya
la source
9

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

Configurez deux ensembles de contraintes similaires à la réponse de Jorge Arimany et TMin:

entrez la description de l'image ici

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

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

constraintB.priority = .defaultHigh

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

constraintB.priority = .defaultLow

L'avantage (certes mineur) de cette approche par rapport au simple changement isActivede 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.

SeanR
la source
Cela présente les avantages de ne pas avoir de valeurs en double dans le Storyboard et le code tels que la hauteur / largeur de l'élément. Très élégant.
Robin Daugherty
Merci!! Ça m'a aidé
Parth Barot
Grande approche, merci
Basil
5

Sous-classez la vue et remplacez-la func intrinsicContentSize() -> CGSize. Revenez simplement CGSizeZerosi la vue est masquée.

Daniel
la source
2
C'est bien. Cependant, certains éléments de l'interface utilisateur ont besoin d'un déclencheur invalidateIntrinsicContentSize. Vous pouvez le faire dans un remplacement setHidden.
DrMickeyLauer
5

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

Connaître ce fait / bogue pourrait aider certaines personnes à simplifier leur mise en page, peut-être même celle de la question d'origine, alors j'ai pensé que je la posterais.

Matt Koala
la source
1
Je ne pense pas que vous ayez besoin de le cacher. Définir son texte sur une chaîne vide devrait suffire.
Rob VS
4

Je suis surpris qu'il n'y ait pas d'approche plus élégante UIKitpour ce comportement souhaité. Il semble être une chose très courante de vouloir pouvoir faire.

Depuis la connexion des contraintes à IBOutletset la définition de leurs constantes sur une 0sensation dégueulasse (et ayant provoqué des NSLayoutConstraintavertissements lorsque votre vue avait des sous-vues), j'ai décidé de créer une extension qui donne une approche simple et dynamique pour masquer / afficher un UIViewqui a des contraintes de mise en page automatique

Il masque simplement la vue et supprime les contraintes extérieures. Lorsque vous affichez à nouveau la vue, elle rajoute les contraintes. 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 inférieurs. Dans iOS 9, utilisez simplement l' UIStackViewapproche.

Albert Bori
la source
1
C'est aussi mon approche préférée. C'est une amélioration significative par rapport aux autres réponses ici, car cela ne fait pas que réduire la vue à la taille zéro. L'approche du squash aura des problèmes pour tout sauf des paiements assez insignifiants. Un exemple de ceci est le grand espace où l'élément caché se trouve dans cette autre réponse . Et vous pouvez vous retrouver avec des violations de contraintes comme mentionné.
Benjohn
@DanielSchlaug Merci de l'avoir signalé. J'ai mis à jour la licence vers le MIT. Cependant, j'ai besoin d'un high-five mental chaque fois que quelqu'un met en œuvre cette solution.
Albert Bori
4

La meilleure pratique consiste, une fois que tout a les bonnes contraintes de mise en page, ajouter une hauteur ou avec une contrainte, selon la façon dont vous voulez que les vues environnantes se déplacent et connectent la contrainte dans une IBOutletpropriété.

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

dans le code, il vous suffit de définir la constante sur 0 et de l' activer , puis de masquer le contenu ou de le désactiver pour afficher le contenu. C'est mieux que de gâcher la valeur constante et de la restaurer. N'oubliez pas d'appeler layoutIfNeededensuite.

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 que vous avez votre configuration, vous pouvez la tester dans IntefaceBuilder en définissant votre contrainte sur 0, puis en revenant à la valeur d'origine. N'oubliez pas de vérifier les priorités des autres contraintes pour éviter tout conflit lorsqu'elles sont cachées. 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 à la priorité la plus élevée.

Jorge Arimany
la source
1
En effet, la contrainte et la vue ne peuvent pas toujours être conservées par la hiérarchie de vues.Par conséquent, si vous désactivez la contrainte et masquez la vue, vous les conservez toujours pour les afficher à nouveau indépendamment de ce que votre hiérarchie de vues décide de conserver ou non.
Jorge Arimany
2

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

Je préfère créer de multiples contraintes. Créez d'abord vos contraintes lorsque la deuxième étiquette est visible. Créez une sortie pour la contrainte entre le bouton et la deuxième étiquette (si vous utilisez objc, assurez-vous qu'elle est forte). 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 une prise pour la deuxième contrainte et assurez-vous que cette prise a un pointeur fort. Décochez ensuite la installedcase dans le générateur d'interface et assurez-vous que la priorité de la première contrainte est inférieure à cette deuxième priorité de contraintes.

Enfin, lorsque vous masquez la deuxième étiquette, basculez la .isActivepropriété de ces contraintes et appelezsetNeedsDisplay()

Et c'est tout, pas de nombres Magic, pas de mathématiques, si vous avez plusieurs contraintes à activer et à désactiver, vous pouvez même utiliser des collections de points de vente pour les organiser par état. (AKA conserve toutes les contraintes cachées dans un OutletCollection et les 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 de contraintes multiples, mais j'ai l'impression que ce n'est pas de la microgestion, et qu'il est plus simple que de créer dynamiquement des vues et des contraintes par programme (ce que je pense qu'il voulait éviter si je Je lis bien 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()
    }
}

entrez la description de l'image ici

TMin
la source
0

Je vais également fournir ma solution, pour offrir de la variété.) Je pense que créer une sortie pour la largeur / hauteur de chaque élément plus l'espacement est tout 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 UIImageView), sélectionne celles qui doivent être rajoutées et, dans une boucle, elle les rajoute chacune 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 disposition et un espacement propres et cohérents. Mon code utilise Masonry, je le recommande fortement: https://github.com/SnapKit/Masonry

Zoltán
la source
0

Essayez BoxView , il rend la mise en page dynamique concise et lisible.
Dans votre cas c'est:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Vladimir
la source