Come ridimensionare la superview per adattarla a tutte le subview con autolayout?


142

La mia comprensione dell'autolayout è che prende le dimensioni di superview e si basa su vincoli e dimensioni intrinseche che calcola le posizioni delle subview.

C'è un modo per invertire questo processo? Voglio ridimensionare la superview sulla base di vincoli e dimensioni intrinseche. Qual è il modo più semplice per raggiungere questo obiettivo?

Ho una vista progettata in Xcode che uso come intestazione UITableView. Questa vista include un'etichetta e un pulsante. Le dimensioni dell'etichetta variano in base ai dati. A seconda dei vincoli, l'etichetta spinge correttamente il pulsante verso il basso o se esiste un vincolo tra il pulsante e la parte inferiore della superview, l'etichetta viene compressa.

Ho trovato alcune domande simili ma non hanno risposte buone e facili.


28
È necessario selezionare una delle risposte di seguito, in quanto Tom Swifts ha risposto alla tua domanda. Tutti i poster hanno impiegato molto tempo per aiutarti, ora dovresti fare la tua parte e selezionare la risposta che ti piace di più.
David H,

Risposte:


149

L'API corretta da utilizzare è UIView systemLayoutSizeFittingSize:, passando UILayoutFittingCompressedSizeo UILayoutFittingExpandedSize.

Per un normale UIViewautolayout dovrebbe funzionare solo se i tuoi vincoli sono corretti. Se vuoi usarlo su un UITableViewCell(per determinare l'altezza della riga, ad esempio), dovresti chiamarlo contro la tua cella contentViewe afferrare l'altezza.

Ulteriori considerazioni esistono se si hanno una o più UILabel nella propria vista che sono multilinea. Per questi è imperativo che la preferredMaxLayoutWidthproprietà sia impostata correttamente in modo tale che l'etichetta fornisca un valore corretto intrinsicContentSize, che verrà utilizzato nel systemLayoutSizeFittingSize'scalcolo.

MODIFICA: su richiesta, aggiungendo un esempio di calcolo dell'altezza per una cella della vista tabella

L'uso del layout automatico per il calcolo dell'altezza della cella non è molto efficiente ma è sicuramente conveniente, soprattutto se si dispone di una cella con un layout complesso.

Come ho detto sopra, se stai usando una multilinea UILabelè fondamentale sincronizzare la preferredMaxLayoutWidthlarghezza dell'etichetta. Uso una UILabelsottoclasse personalizzata per fare questo:

@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

Ecco una sottoclasse di UITableViewController inventata che dimostra 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

Una semplice cella personalizzata:

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

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

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

@end

Ed ecco una foto dei vincoli definiti nello Storyboard. Si noti che non ci sono vincoli di altezza / larghezza sull'etichetta - questi sono dedotti da quelli dell'etichetta intrinsicContentSize:

inserisci qui la descrizione dell'immagine


1
Puoi fare un esempio di come sarebbe un'implementazione di heightForRowAtIndexPath: usare questo metodo con una cella contenente un'etichetta multilinea? L'ho rovinato un bel po 'e non sono riuscito a farlo funzionare. Come si ottiene una cella (soprattutto se la cella è impostata in uno storyboard)? Quali sono i vincoli necessari per farlo funzionare?
rdelmar,

@rdelmar - certo. Ho aggiunto un esempio alla mia risposta.
TomSwift

7
Questo non ha funzionato per me fino a quando non ho aggiunto un vincolo verticale finale dal fondo della cella al fondo della sottoview più bassa nella cella. Sembra che i vincoli verticali debbano includere la spaziatura verticale superiore e inferiore tra la cella e il suo contenuto affinché il calcolo dell'altezza della cella abbia esito positivo.
Eric Baker,

1
Mi serviva solo un altro trucco per farlo funzionare. E la punta di @EricBaker l'ha finalmente inchiodata. Grazie per aver condiviso quell'uomo. Ora ho una dimensione diversa da zero o contro la sizingCello la sua contentView.
McVal,

1
Per coloro che hanno difficoltà a non raggiungere la giusta altezza, assicurati che la sizingCelllarghezza della tua larghezza corrisponda alla tua tableView.
McVal,

30

Il commento di Eric Baker mi ha portato all'idea principale che , affinché una vista abbia la sua dimensione determinata dal contenuto collocato al suo interno, il contenuto inserito al suo interno deve avere una relazione esplicita con la vista contenuta al fine di determinarne l'altezza (o larghezza) in modo dinamico . "Aggiungi sottoview" non crea questa relazione come si potrebbe presumere. Devi scegliere quale sottoview guiderà l'altezza e / o la larghezza del contenitore ... più comunemente qualunque elemento dell'interfaccia utente che hai posizionato nell'angolo in basso a destra dell'interfaccia utente generale. Ecco un po 'di codice e commenti incorporati per illustrare il punto.

Nota, questo può essere di particolare valore per coloro che lavorano con le viste di scorrimento poiché è comune progettare attorno a una singola vista del contenuto che determina le sue dimensioni (e le comunica alla vista di scorrimento) in modo dinamico in base a ciò che hai inserito. Buona fortuna, spero che questo aiuti qualcuno là fuori.

//
//  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

Come ridimensionare la superview per adattarla a tutte le subview con autolayout.png


3
Questo è un grande trucco e non molto conosciuto. Per ripetere: se le viste interne hanno un'altezza intrinseca e sono fissate in alto e in basso, la vista esterna non deve specificare la sua altezza e, di fatto, abbraccerà il suo contenuto. Potrebbe essere necessario regolare la compressione e l'abbraccio del contenuto per le visualizzazioni interne per ottenere i risultati desiderati.
Phatmann,

Questo è denaro! Uno dei migliori esempi concisi di VFL che abbia mai visto.
Evan R,

Questo è quello che stavo cercando (grazie a John), quindi ho pubblicato una versione Swift di questo qui
James,

1
"Devi scegliere quale sottoview guiderà l'altezza e / o la larghezza del contenitore ... più comunemente qualsiasi elemento dell'interfaccia utente che hai posizionato nell'angolo in basso a destra dell'interfaccia utente complessiva" .. Sto usando PureLayout, e questa è stata la chiave per me. Per una vista padre, con una vista figlio, era seduta la vista figlio da appuntare in basso a destra che improvvisamente dava una dimensione alla vista padre. Grazie!
Steven Elliott,

1
Scegliere una vista per guidare la larghezza è esattamente ciò che non posso fare. A volte una sottoview è più ampia, a volte un'altra sottoview è più ampia. Qualche idea per quel caso?
Henning,

3

Puoi farlo creando un vincolo e collegandolo tramite il builder di interfacce

Vedi spiegazione: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich inizio-layout automatico

Nozioni di base sugli articoli di AutolayoutPG

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

connetti questo outlet Vincolo con le tue visualizzazioni secondarie Vincolo o connetti anche le visualizzazioni super Vincolo e impostalo in base alle tue esigenze come questa

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

Spero che questo lo chiarisca.


Quindi è possibile configurare i vincoli creati in XCode dal codice sorgente. Ma la domanda è come configurarli per ridimensionare la superview.
DAK,

SÌ @DAK. assicurati del layout della superview. Metti Vincolo in superview e crea anche Vincolo in altezza, così ogni volta che la tua subview aumenta automaticamente aumenta l'altezza di Vincolo in superview.
chandan,

Questo ha funzionato anche per me. Mi aiuta a disporre di celle di dimensioni fisse (altezza di 60 px). Quindi quando la vista viene caricata, ho impostato il mio vincolo di altezza IBOutlet su 60 * x dove x è il numero di celle. Alla fine, probabilmente lo vorresti in una vista di scorrimento in modo da poter vedere l'intera cosa.
Sandy Chapman,

-1

Questo può essere fatto per un normale subviewall'interno di un più grande UIView, ma non funziona automaticamente per headerViews. L'altezza di a headerViewè determinata da ciò che viene restituito, tableView:heightForHeaderInSection:quindi devi calcolare in heightbase heightallo UILabelspazio più per UIButtone quello che paddingti serve. Devi fare qualcosa del genere:

-(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;
}

Ecco la headerStringstringa che vuoi popolare UILabel, e il numero 281 è il widthdel UILabel(come impostato in Interface Builder)


Purtroppo questo non funziona. La funzione "Adatta al contenuto" sulla superview rimuove il vincolo che collega la parte inferiore del pulsante alla superview come previsto. In fase di esecuzione l'etichetta viene ridimensionata e spinge il pulsante verso il basso ma la superview non viene ridimensionata automaticamente.
DAK,

@DAK, scusa, il problema è che la tua vista è l'intestazione per una vista tabella. Ho frainteso la tua situazione. Pensavo che la vista che racchiudesse il pulsante e l'etichetta fosse all'interno di un'altra vista, ma sembra che tu lo stia usando come intestazione (piuttosto che essere una vista all'interno dell'intestazione). Quindi, la mia risposta originale non funzionerà. Ho cambiato la mia risposta a ciò che penso dovrebbe funzionare.
rdelmar,

questo è simile alla mia attuale implementazione ma speravo che ci fosse un modo migliore. Preferirei evitare di aggiornare questo codice ogni volta che cambio intestazione.
DAK,

@Dak, scusami, non penso che ci sia un modo migliore, è solo il modo in cui funzionano le viste delle tabelle.
rdelmar,

Si prega di vedere la mia risposta. Il "modo migliore" è utilizzare UIView systemLayoutSizeFittingSize :. Detto questo, eseguire un autolayout completo su una vista o una cella solo per ottenere l'altezza richiesta in una vista tabella è piuttosto costoso. Lo farei per celle complesse, ma forse ricorrere a una soluzione come la tua per casi più semplici.
TomSwift
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.