Como redimensionar a superview para caber em todas as subviews com o autolayout?

142

Meu entendimento sobre o autolayout é que ele assume o tamanho da superview e se baseia em restrições e tamanhos intrínsecos que calcula as posições das subviews.

Existe uma maneira de reverter esse processo? Eu quero redimensionar a super visão com base em restrições e tamanhos intrínsecos. Qual é a maneira mais simples de conseguir isso?

Eu tenho vista projetada no Xcode que eu uso como um cabeçalho para UITableView. Essa visualização inclui um rótulo e um botão. O tamanho da etiqueta varia de acordo com os dados. Dependendo das restrições, a etiqueta pressiona o botão com sucesso ou, se houver uma restrição entre o botão e a parte inferior da supervisão, a etiqueta é compactada.

Encontrei algumas perguntas semelhantes, mas elas não têm respostas boas e fáceis.

DAK
fonte
28
Você deve selecionar uma das respostas abaixo, para garantir que Tom Swifts respondeu à sua pergunta. Todos os pôsteres gastaram uma quantidade enorme de tempo para ajudá-lo, agora você deve fazer sua parte e selecionar a resposta que mais lhe agrada.
David H

Respostas:

149

A API correta a ser usada é UIView systemLayoutSizeFittingSize:passar um UILayoutFittingCompressedSizeou UILayoutFittingExpandedSize.

Para um UIViewuso normal do autolayout, isso deve funcionar apenas enquanto suas restrições estiverem corretas. Se você quiser usá-lo em um UITableViewCell(para determinar a altura da linha, por exemplo), deve chamá-lo no seu celularcontentView e pegar a altura.

Considerações adicionais existem se você tiver um ou mais UILabels em sua exibição que são multilinhas. Para estes, é imperativo que a preferredMaxLayoutWidthpropriedade seja definida corretamente, de modo que o rótulo forneça um correto intrinsicContentSize, que será usado no systemLayoutSizeFittingSize'scálculo.

EDIT: por solicitação, adicionando exemplo de cálculo de altura para uma célula de exibição de tabela

Usar o layout automático para o cálculo da altura da célula da tabela não é super eficiente, mas certamente é conveniente, especialmente se você tiver uma célula com um layout complexo.

Como eu disse acima, se você estiver usando uma multilinha UILabel, é essencial sincronizar a preferredMaxLayoutWidthlargura da etiqueta. Eu uso uma UILabelsubclasse personalizada para fazer isso:

@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

Aqui está uma subclasse artificial de UITableViewController que demonstra 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

Uma célula personalizada simples:

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

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

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

@end

E, aqui está uma imagem das restrições definidas no Storyboard. Observe que não há restrições de altura / largura no rótulo - elas são deduzidas dos rótulos intrinsicContentSize:

insira a descrição da imagem aqui

TomSwift
fonte
1
Você pode dar um exemplo de como seria a implementação de heightForRowAtIndexPath: usando esse método com uma célula contendo um rótulo de várias linhas? Eu mexi um pouco com isso e não consegui que funcionasse. Como você obtém uma célula (especialmente se a célula estiver configurada em um storyboard)? Quais restrições você precisa para fazer funcionar?
Rdelmar
@rdelmar - com certeza. Eu adicionei um exemplo à minha resposta.
precisa saber é o seguinte
7
Isso não funcionou para mim até eu adicionar uma restrição vertical final da parte inferior da célula à parte inferior da subvisão mais baixa da célula. Parece que as restrições verticais devem incluir o espaçamento vertical superior e inferior entre a célula e seu conteúdo para que o cálculo da altura da célula seja bem-sucedido.
Eric Baker
1
Eu só precisava de mais um truque para fazê-lo funcionar. E a dica de @EricBaker finalmente acertou em cheio. Obrigado por compartilhar esse homem. Eu tenho tamanho diferente de zero agora, contra sizingCellou o seu contentView.
MkVal
1
Para aqueles que não conseguem obter a altura certa, verifique se a sizingCelllargura corresponde à sua tableViewlargura.
MkVal
30

O comentário de Eric Baker me sugeriu a idéia principal de que , para que uma exibição tenha seu tamanho determinado pelo conteúdo colocado nela, o conteúdo colocado nela deve ter um relacionamento explícito com a exibição que contém a fim de aumentar sua altura (ou largura) dinamicamente . "Adicionar subvisão" não cria esse relacionamento, como você pode imaginar. Você precisa escolher qual subvisão direcionará a altura e / ou largura do contêiner ... mais comumente qualquer elemento da interface do usuário que você colocou no canto inferior direito da interface do usuário geral. Aqui estão alguns comentários embutidos e de código para ilustrar o ponto.

Observe que isso pode ser de particular valor para quem trabalha com visualizações de rolagem, pois é comum projetar em torno de uma única visualização de conteúdo que determina seu tamanho (e comunica isso à visualização de rolagem) dinamicamente, com base no que você coloca nela. Boa sorte, espero que isso ajude alguém lá fora.

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

Como redimensionar a superview para ajustar todas as subviews com autolayout.png

John Erck
fonte
3
Este é um grande truque e não é bem conhecido. Para repetir: se as vistas internas tiverem uma altura intrínseca e estiverem fixadas na parte superior e inferior, a vista externa não precisará especificar sua altura e, de fato, envolverá seu conteúdo. Pode ser necessário ajustar a compactação e o ajuste do conteúdo para que as visualizações internas obtenham os resultados desejados.
Phatmann
Isso é dinheiro! Um dos melhores exemplos concisos de VFL que já vi.
Evan R
Isto é o que eu estava procurando (Obrigado, @John), por isso publiquei uma versão Swift aqui
James James
1
"Você precisa escolher qual subvisão direcionará a altura e / ou largura do contêiner ... mais comumente qualquer elemento da interface do usuário que você colocou no canto inferior direito da interface do usuário geral" .. Estou usando o PureLayout, e essa foi a chave para mim. Para uma visão pai, com uma visão filho, estava sentada a visão filho para fixar no canto inferior direito que de repente deu um tamanho à visão pai. Obrigado!
Steven Elliott
1
Escolher uma vista para aumentar a largura é exatamente o que não posso fazer. Às vezes, uma subvisão é maior, outras vezes, outra subview. Alguma idéia para esse caso?
Henning
3

Você pode fazer isso criando uma restrição e conectando-a através do construtor de interface

Veja a explicação: Auto_Layout_Constraints_in_Interface_Builder

raywenderlich começando-auto-layout

Restrição de artigos do AutolayoutPG Fundamentals

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

conecte esta tomada de restrições com suas sub-visualizações Restrição ou conecte super-vistas também com restrições e defina-o de acordo com seus requisitos como este

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

Espero que isso esclareça.

Chandan
fonte
Portanto, é possível configurar restrições criadas no XCode a partir do código fonte. Mas a questão é como configurá-los para redimensionar a superview.
DAK
SIM @DAK. verifique o layout da superview. Coloque Restrição na superview e faça a restrição de altura também, sempre que sua subview aumentar, automaticamente aumente a altura da superview.
chandan
Isso também funcionou para mim. Ajuda que eu tenho células de tamanho fixo (altura de 60px). Portanto, quando a exibição é carregada, defino minha restrição de altura do IBOutlet como 60 * x, em que x é o número de células. Por fim, você provavelmente desejaria isso em uma exibição de rolagem para poder ver a coisa toda.
Sandy Chapman
-1

Isso pode ser feito para um normal subviewdentro de um maior UIView, mas não funciona automaticamente headerViews. A altura de um headerViewé determinada pelo que é retornado por tableView:heightForHeaderInSection:isso você tem que calcular o heightbaseado no heightdo UILabelespaço vantagem para o UIButtone qualquer paddingque você precisa. Você precisa fazer algo assim:

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

Aqui headerStringestá a sequência que você deseja preencher UILabele o número 281 é o widthde UILabel(como configurado em Interface Builder)

rdelmar
fonte
Infelizmente isso não funciona. “Tamanho para ajustar ao conteúdo” na superview remove a restrição que conecta a parte inferior do botão à superview como você previu. No tempo de execução, o rótulo é redimensionado e pressiona o botão, mas a superview não é redimensionada automaticamente.
DAK
@ DAK, desculpe, o problema é que sua visualização é o cabeçalho de uma visualização de tabela. Eu não entendi sua situação. Eu estava pensando que a exibição que continha o botão e o rótulo estava dentro de outra exibição, mas parece que você está usando o cabeçalho (em vez de ser uma exibição dentro do cabeçalho). Portanto, minha resposta original não funcionará. Mudei minha resposta para o que acho que deveria funcionar.
Rdelmar 9/08
isso é semelhante à minha implementação atual, mas eu esperava que houvesse uma maneira melhor. Eu preferiria evitar atualizar esse código sempre que alterar meu cabeçalho.
DAK
@ Dak, desculpe, não acho que exista uma maneira melhor, é assim que as exibições da tabela funcionam.
Rdelmar
Por favor, veja minha resposta. A "melhor maneira" é usar o UIView systemLayoutSizeFittingSize :. Dito isso, executar um autolayout completo em uma exibição ou célula apenas para obter a altura necessária em uma tableview é bastante caro. Eu faria isso para células complexas, mas talvez recorresse a uma solução como a sua para casos mais simples.
TomSwift