Comment redimensionner superview pour s'adapter à toutes les sous-vues avec mise en page automatique?

142

Ma compréhension de la mise en page automatique est qu'elle prend la taille de la super-vue et qu'elle se base sur des contraintes et des tailles intrinsèques, elle calcule les positions des sous-vues.

Existe-t-il un moyen d'inverser ce processus? Je souhaite redimensionner superview sur la base des contraintes et des tailles intrinsèques. Quelle est la manière la plus simple d'y parvenir?

J'ai une vue conçue dans Xcode que j'utilise comme en-tête pour UITableView. Cette vue comprend une étiquette et un bouton. La taille de l'étiquette varie en fonction des données. En fonction des contraintes, l'étiquette pousse avec succès le bouton vers le bas ou s'il existe une contrainte entre le bouton et le bas de la vue supervisée, l'étiquette est compressée.

J'ai trouvé quelques questions similaires mais elles n'ont pas de bonnes réponses faciles.

DAK
la source
28
Vous devez sélectionner l'une des réponses ci-dessous, car Tom Swifts a certainement répondu à votre question. Toutes les affiches ont passé énormément de temps à vous aider, maintenant vous devez faire votre part et sélectionner la réponse que vous préférez.
David H du

Réponses:

149

L'API correcte à utiliser est UIView systemLayoutSizeFittingSize:, en passant soit UILayoutFittingCompressedSizeou UILayoutFittingExpandedSize.

Pour une mise UIViewen page automatique normale, cela devrait fonctionner tant que vos contraintes sont correctes. Si vous souhaitez l'utiliser sur un UITableViewCell(pour déterminer la hauteur de ligne par exemple), vous devez l'appeler contre votre cellule contentViewet saisir la hauteur.

D'autres considérations existent si vous avez un ou plusieurs UILabel dans votre vue qui sont multilignes. Pour ceux-ci, il est impératif que la preferredMaxLayoutWidthpropriété soit correctement définie de sorte que l'étiquette fournisse un correct intrinsicContentSize, qui sera utilisé dans le systemLayoutSizeFittingSize'scalcul.

EDIT: sur demande, ajout d'un exemple de calcul de hauteur pour une cellule de vue tableau

L'utilisation de la disposition automatique pour le calcul de la hauteur des cellules d'un tableau n'est pas très efficace, mais elle est certainement pratique, surtout si vous avez une cellule qui a une disposition complexe.

Comme je l'ai dit ci-dessus, si vous utilisez une multiligne, UILabelil est impératif de synchroniser le preferredMaxLayoutWidthavec la largeur de l'étiquette. J'utilise une UILabelsous-classe personnalisée pour ce faire:

@implementation TSLabel

- (void) layoutSubviews
{
    [super layoutSubviews];

    if ( self.numberOfLines == 0 )
    {
        if ( self.preferredMaxLayoutWidth != self.frame.size.width )
        {
            self.preferredMaxLayoutWidth = self.frame.size.width;
            [self setNeedsUpdateConstraints];
        }
    }
}

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    if ( self.numberOfLines == 0 )
    {
        // found out that sometimes intrinsicContentSize is 1pt too short!
        s.height += 1;
    }

    return s;
}

@end

Voici une sous-classe UITableViewController artificielle démontrant heightForRowAtIndexPath:

#import "TSTableViewController.h"
#import "TSTableViewCell.h"

@implementation TSTableViewController

- (NSString*) cellText
{
    return @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
    return 1;
}

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static TSTableViewCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell"];
    });

    // configure the cell
    sizingCell.text = self.cellText;

    // force layout
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    // get the fitting size
    CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
    NSLog( @"fittingSize: %@", NSStringFromCGSize( s ));

    return s.height;
}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell" ];

    cell.text = self.cellText;

    return cell;
}

@end

Une simple cellule personnalisée:

#import "TSTableViewCell.h"
#import "TSLabel.h"

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

- (void) setText: (NSString *) text
{
    _label.text = text;
}

@end

Et voici une image des contraintes définies dans le Storyboard. Notez qu'il n'y a pas de contraintes de hauteur / largeur sur l'étiquette - celles-ci sont déduites des étiquettes intrinsicContentSize:

entrez la description de l'image ici

TomSwift
la source
1
Pouvez-vous donner un exemple de ce à quoi une implémentation de heightForRowAtIndexPath: ressemblerait en utilisant cette méthode avec une cellule contenant une étiquette multiligne? J'ai pas mal joué avec ça et je ne l'ai pas fait fonctionner. Comment obtenez-vous une cellule (surtout si la cellule est configurée dans un storyboard)? De quelles contraintes avez-vous besoin pour le faire fonctionner?
rdelmar
@rdelmar - bien sûr. J'ai ajouté un exemple à ma réponse.
TomSwift
7
Cela ne fonctionnait pas pour moi jusqu'à ce que j'aie ajouté une dernière contrainte verticale du bas de la cellule au bas de la sous-vue la plus basse de la cellule. Il semble que les contraintes verticales doivent inclure l'espacement vertical haut et bas entre la cellule et son contenu pour que le calcul de la hauteur de la cellule réussisse.
Eric Baker
1
Je n'avais besoin que d'une astuce de plus pour le faire fonctionner. Et le conseil de @EricBaker a finalement réussi. Merci d'avoir partagé cet homme. J'ai maintenant une taille non nulle contre le sizingCellou le sien contentView.
MkVal
1
Pour ceux qui ont du mal à ne pas obtenir la bonne hauteur, assurez-vous que votre sizingCelllargeur correspond à la vôtre tableView.
MkVal
30

Le commentaire d'Eric Baker m'a fait remonter à l'idée de base que pour qu'une vue ait sa taille déterminée par le contenu qui y est placé, alors le contenu qui y est placé doit avoir une relation explicite avec la vue contenante afin de conduire sa hauteur. (ou largeur) dynamiquement . "Ajouter une sous-vue" ne crée pas cette relation comme vous pourriez le supposer. Vous devez choisir la sous-vue qui pilotera la hauteur et / ou la largeur du conteneur ... le plus souvent, quel que soit l'élément d'interface utilisateur que vous avez placé dans le coin inférieur droit de votre interface utilisateur globale. Voici du code et des commentaires en ligne pour illustrer ce point.

Notez que cela peut être particulièrement utile pour ceux qui travaillent avec des vues de défilement car il est courant de concevoir autour d'une seule vue de contenu qui détermine sa taille (et la communique à la vue de défilement) de manière dynamique en fonction de ce que vous y mettez. Bonne chance, j'espère que cela aidera quelqu'un là-bas.

//
//  ViewController.m
//  AutoLayoutDynamicVerticalContainerHeight
//

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) UILabel *myLabel;
@property (strong, nonatomic) UILabel *myOtherLabel;
@end

@implementation ViewController

- (void)viewDidLoad
{
    // INVOKE SUPER
    [super viewDidLoad];

    // INIT ALL REQUIRED UI ELEMENTS
    self.contentView = [[UIView alloc] init];
    self.myLabel = [[UILabel alloc] init];
    self.myOtherLabel = [[UILabel alloc] init];
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);

    // TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // ESTABLISH VIEW HIERARCHY
    [self.view addSubview:self.contentView]; // View adds content view
    [self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
    [self.contentView addSubview:self.myOtherLabel];

    // LAYOUT

    // Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
    // Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet

    /* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
    // ^To me this is what's weird... but okay once you understand...

    // Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];

    // Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
    // Note, this is the view that we are choosing to use to drive the height (and width) of our container...

    // The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // COLOR VIEWS
    self.view.backgroundColor = [UIColor purpleColor];
    self.contentView.backgroundColor = [UIColor redColor];
    self.myLabel.backgroundColor = [UIColor orangeColor];
    self.myOtherLabel.backgroundColor = [UIColor greenColor];

    // CONFIGURE VIEWS

    // Configure MY LABEL
    self.myLabel.text = @"HELLO WORLD\nLine 2\nLine 3, yo";
    self.myLabel.numberOfLines = 0; // Let it flow

    // Configure MY OTHER LABEL
    self.myOtherLabel.text = @"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
    self.myOtherLabel.numberOfLines = 0;
    self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}

@end

Comment redimensionner Superview pour s'adapter à toutes les sous-vues avec autolayout.png

John Erck
la source
3
C'est une super astuce et peu connue. Pour répéter: si les vues intérieures ont une hauteur intrinsèque et sont épinglées en haut et en bas, la vue extérieure n'a pas besoin de spécifier sa hauteur et épousera en fait son contenu. Vous devrez peut-être ajuster la compression et l'étreinte du contenu pour les vues intérieures afin d'obtenir les résultats souhaités.
phatmann le
C'est de l'argent! L'un des exemples de VFL les plus concis que j'ai vus.
Evan R
C'est ce que je cherchais (Merci @John) alors j'ai publié une version Swift de ceci ici
James
1
"Vous devez choisir la sous-vue qui pilotera la hauteur et / ou la largeur du conteneur ... le plus souvent, quel que soit l'élément d'interface utilisateur que vous avez placé dans le coin inférieur droit de votre interface utilisateur globale" .. J'utilise PureLayout, et c'était la clé pour moi. Pour une vue parent, avec une vue enfant, c'était l'assise de la vue enfant à épingler en bas à droite qui a soudainement donné une taille à la vue parent. Merci!
Steven Elliott
1
Choisir une vue pour conduire la largeur est exactement ce que je ne peux pas faire. Parfois, une sous-vue est plus large, parfois une autre sous-vue est plus large. Des idées pour ce cas?
Henning
3

Vous pouvez le faire en créant une contrainte et en la connectant via le générateur d'interface

Voir l'explication: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich début-auto-layout

Principes de base des contraintes des articles AutolayoutPG

@interface ViewController : UIViewController {
    IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
    IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;

connectez cette prise de contrainte avec vos sous-vues Contrainte ou connectez également des super vues Contrainte et définissez-la en fonction de vos besoins comme ceci

 self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign

J'espère que cela clarifie la situation.

Chandan
la source
Il est donc possible de configurer des contraintes créées dans XCode à partir du code source. Mais la question est de savoir comment les configurer pour redimensionner superview.
DAK
OUI @DAK. veuillez vous assurer de la mise en page superview. Mettez la contrainte sur superview et créez également une contrainte de hauteur, de sorte que chaque fois que votre sous-vue augmente, elle augmente automatiquement la hauteur de la contrainte de supervision.
chandan
Cela a également fonctionné pour moi. Cela aide d'avoir des cellules de taille fixe (hauteur de 60 pixels). Ainsi, lorsque la vue se charge, j'ai défini ma contrainte de hauteur IBOutlet sur 60 * x où x est le nombre de cellules. En fin de compte, vous voudrez probablement cela dans une vue de défilement afin que vous puissiez voir le tout.
Sandy Chapman
-1

Cela peut être fait pour une normale subviewà l' intérieur d'un plus grand UIView, mais cela ne fonctionne pas automatiquement pour headerViews. La hauteur de a headerViewest déterminée par ce qui est renvoyé par tableView:heightForHeaderInSection:, vous devez donc calculer le en heightfonction heightde l' UILabelespace plus pour le UIButtonet tout paddingce dont vous avez besoin. Vous devez faire quelque chose comme ceci:

-(CGFloat)tableView:(UITableView *)tableView 
          heightForHeaderInSection:(NSInteger)section {
    NSString *s = self.headeString[indexPath.section];
    CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17] 
                constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
                    lineBreakMode:NSLineBreakByWordWrapping];
    return size.height + 60;
}

Voici la headerStringchaîne que vous souhaitez remplir UILabel, et le numéro 281 est le widthde UILabel(comme configuré dans Interface Builder)

rdelmar
la source
Malheureusement, cela ne fonctionne pas. «Adapter le contenu au contenu» de la vue supervisée supprime la contrainte qui relie le bas du bouton à la vue supervisée comme vous l'avez prédit. Au moment de l'exécution, l'étiquette est redimensionnée et il pousse le bouton vers le bas, mais la vue supervisée n'est pas automatiquement redimensionnée.
DAK
@DAK, désolé, le problème est que votre vue est l'en-tête d'une vue de tableau. J'ai mal compris votre situation. Je pensais que la vue englobant le bouton et l'étiquette était dans une autre vue, mais il semble que vous l'utilisiez comme en-tête (plutôt que d'être une vue à l'intérieur de l'en-tête). Donc, ma réponse originale ne fonctionnera pas. J'ai changé ma réponse à ce que je pense devrait fonctionner.
rdelmar
ceci est similaire à mon implémentation actuelle mais j'espérais qu'il existe un meilleur moyen. Je préférerais éviter de mettre à jour ce code à chaque fois que je change d'en-tête.
DAK
@Dak, Désolé, je ne pense pas qu'il y ait une meilleure façon, c'est juste la façon dont les vues de table fonctionnent.
rdelmar
S'il vous plaît voir ma réponse. La "meilleure façon" est d'utiliser UIView systemLayoutSizeFittingSize :. Cela dit, effectuer une mise en page automatique complète sur une vue ou une cellule juste pour obtenir la hauteur requise dans une vue de table est assez coûteux. Je le ferais pour les cellules complexes, mais peut-être utiliser une solution comme la vôtre pour les cas plus simples.
TomSwift