AutoLayout com UIViews ocultos?


164

Sinto que é um paradigma bastante comum mostrar / ocultar UIViews, na maioria das vezes UILabels, dependendo da lógica de negócios. Minha pergunta é: qual é a melhor maneira de usar o AutoLayout para responder a visualizações ocultas como se seu quadro fosse 0x0. Aqui está um exemplo de uma lista dinâmica de 1-3 recursos.

Lista de recursos dinâmicos

No momento, tenho um espaço superior de 10 px do botão até o último rótulo, o que obviamente não desliza para cima quando o rótulo está oculto. A partir de agora, criei uma saída para essa restrição e modifique a constante, dependendo de quantos rótulos estou exibindo. Obviamente, isso é um pouco invasivo, pois estou usando valores constantes negativos para pressionar o botão sobre os quadros ocultos. Também é ruim porque não está sendo restrito a elementos de layout reais, apenas cálculos estáticos furtivos com base em alturas / preenchimentos conhecidos de outros elementos e obviamente lutando contra o que o AutoLayout foi criado.

Obviamente, eu poderia simplesmente criar novas restrições, dependendo dos meus rótulos dinâmicos, mas isso é muita microgerenciamento e muita verbosidade para tentar recolher apenas alguns espaços em branco. Existem abordagens melhores? Alterar o tamanho do quadro 0,0 e permitir que o AutoLayout faça seu trabalho sem manipulação de restrições? Removendo completamente as visualizações?

Honestamente, apenas modificar a constante do contexto da exibição oculta exige uma única linha de código com cálculo simples. Recriar novas restrições com constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:parece muito pesado.

Editar fev 2018 : Veja a resposta de Ben com UIStackViews


Obrigado Ryan por esta pergunta. Eu estava enlouquecendo o que fazer nos casos, como você pediu. Toda vez que eu verifico o tutorial para o autolayout, a maioria deles diz que consulte o site do tutorial raywenderlich, que acho um pouco difícil de entender.
Nassif

Respostas:


82

UIStackViewé provavelmente o caminho a seguir para o iOS 9+. Além de lidar com a exibição oculta, ele também removerá espaçamento e margens adicionais, se configurados corretamente.


8
Cuidado ao usar o UIStackView em um UITableViewCell. Para mim, uma vez que a visualização ficou muito complicada, a rolagem começou a ficar irregular, onde gaguejava toda vez que uma célula era rolada para dentro / fora. Nos bastidores, o StackView está adicionando / removendo restrições e isso não será necessariamente eficiente o suficiente para uma rolagem suave.
Kento

7
Seria bom dar um pequeno exemplo de como a exibição em pilha lida com isso.
perdida

@Kento ainda é um problema no iOS 10?
Crashalot 31/10/19

3
Cinco anos depois ... devo atualizar e definir isso como a resposta aceita? Está disponível por três ciclos de lançamento agora.
Ryan Romanchuk

1
@RyanRomanchuk Você deveria, esta é definitivamente a melhor resposta agora. Obrigado Ben!
Duncan Luk

234

Minha preferência pessoal por mostrar / ocultar vistas é criar um IBOutlet com a restrição de largura ou altura apropriada.

Em seguida, atualizo o constantvalor para 0ocultar ou qualquer que seja o valor que deve ser exibido.

A grande vantagem dessa técnica é que restrições relativas serão mantidas. Por exemplo, digamos que você tenha a visualização A e a visualização B com um espaço horizontal de x . Quando a vista A largura constantestá definida como 0.fvista, a vista B se move para a esquerda para preencher esse espaço.

Não há necessidade de adicionar ou remover restrições, o que é uma operação pesada. Simplesmente atualizar as restrições constantfará o truque.


1
exatamente. Desde que, neste exemplo, você posicione a vista B à direita da vista A usando um espaço horizontal. Eu uso essa técnica o tempo todo e ele funciona muito bem
Max MacLeod

9
@MaxMacLeod Apenas para ter certeza: se o intervalo entre as duas visualizações for x, para que a visualização B comece na posição da visualização A, também devemos alterar a constante de restrição de gap para 0 também, certo?
Kernix 7/03

1
@ kernix yes se for necessário haver uma restrição de espaço horizontal entre as duas visualizações. constante 0, é claro, não significa lacuna. Portanto, ocultar a vista A, definindo sua largura como 0, moveria a vista B para a esquerda para ficar nivelada com o contêiner. Btw obrigado pelos votos!
Max MacLeod

2
@MaxMacLeod Obrigado pela sua resposta. Tentei fazer isso com um UIView que contém outras visualizações, mas as subvisões dessa visualização não são ocultas quando eu defino sua restrição de altura como 0. Esta solução deve funcionar com visualizações contendo subviews? Obrigado!
shaunlim

6
Apreciaria também uma solução que funciona para um UIView contendo outros pontos de vista ...
Mark Gibaud

96

A solução de usar uma constante 0quando oculta e outra constante se você a mostrar novamente é funcional, mas não é satisfatória se o seu conteúdo tiver um tamanho flexível. Você precisaria medir seu conteúdo flexível e definir um retorno constante. Isso parece errado e tem problemas se o conteúdo mudar de tamanho devido a eventos do servidor ou da interface do usuário.

Eu tenho uma solução melhor.

A idéia é definir a regra de altura 0 para ter alta prioridade quando ocultamos o elemento, para que ele não ocupe espaço no layout automático.

Aqui está como você faz isso:

1. configure uma largura (ou altura) de 0 no construtor de interfaces com baixa prioridade.

regra de largura 0

O Interface Builder não grita sobre conflitos porque a prioridade é baixa. Teste o comportamento da altura definindo temporariamente a prioridade para 999 (1000 é proibido sofrer alterações programáticas, para que não o usemos). O construtor de interfaces provavelmente agora gritará sobre restrições conflitantes. Você pode corrigi-los definindo prioridades em objetos relacionados para 900 ou mais.

2. Adicione uma tomada para poder modificar a prioridade da restrição de largura no código:

conexão de tomada

3. Ajuste a prioridade ao ocultar seu elemento:

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

@SimplGy, você pode me dizer qual é esse lugar?
Niharika

Não é parte da solução geral, @Niharika, é exatamente o que a regra de negócios era no meu caso (mostre a visualização se o restaurante estiver / não fechando em breve).
SimplGy 19/02

11

Nesse caso, mapeio a altura do rótulo do autor para um IBOutlet apropriado:

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

e quando defino a altura da restrição como 0,0f, preservamos o "preenchimento", porque a altura do botão Reproduzir permite isso.

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

insira a descrição da imagem aqui


9

Há muitas soluções aqui, mas minha abordagem usual é diferente novamente :)

Configure dois conjuntos de restrições semelhantes à resposta de Jorge Arimany e TMin:

insira a descrição da imagem aqui

Todas as três restrições marcadas têm o mesmo valor para a constante. As restrições marcadas como A1 e A2 têm prioridade definida como 500, enquanto a restrição marcada como B tem prioridade definida como 250 (ouUILayoutProperty.defaultLow no código).

Conecte a restrição B a um IBOutlet. Então, quando você oculta o elemento, basta definir a prioridade da restrição como alta (750):

constraintB.priority = .defaultHigh

Mas quando o elemento estiver visível, defina a prioridade novamente como baixa (250):

constraintB.priority = .defaultLow

A vantagem (reconhecidamente menor) dessa abordagem em relação à alteração isActiveda restrição B é que você ainda tem uma restrição de trabalho se o elemento transitório for removido da visualização por outros meios.


Isso tem as vantagens de não ter valores duplicados no Storyboard e código como altura / largura do elemento. Muito elegante.
Robin Daugherty

Obrigado!! Ele me ajudou
Parth Barot

Grande Approach, Obrigado
Basil

5

Subclasse a vista e substitua func intrinsicContentSize() -> CGSize. Basta retornar CGSizeZerose a vista estiver oculta.


2
Isso é ótimo. Alguns elementos da interface do usuário precisam de um gatilho para isso invalidateIntrinsicContentSize. Você pode fazer isso de forma anulada setHidden.
DrMickeyLauer

5

Acabei de descobrir que, para obter um UILabel para não ocupar espaço, você deve ocultá-lo E definir seu texto como uma string vazia. (iOS 9)

Conhecer esse fato / bug pode ajudar algumas pessoas a simplificar seus layouts, possivelmente até o da pergunta original, então achei que o postaria.


1
Eu não acho que você precise escondê-lo. Definir seu texto como uma string vazia deve ser suficiente.
Rob VS

4

Estou surpreso que não exista uma abordagem mais elegante fornecida pelo UIKit para esse comportamento desejado. Parece uma coisa muito comum querer poder fazer.

Desde que conectamos as restrições IBOutletse definimos suas constantes para 0que pareçam indecentes (e causou NSLayoutConstraintavisos quando a visualização tinha subvisões), decidi criar uma extensão que fornece uma abordagem simples e com estado de ocultar / mostrar um UIViewque tenha restrições de layout automático

Apenas oculta a vista e remove as restrições externas. Quando você mostra a exibição novamente, ela adiciona as restrições novamente. A única ressalva é que você precisará especificar restrições flexíveis de failover para as vistas circundantes.

Editar Esta resposta está direcionada ao iOS 8.4 e abaixo. No iOS 9, basta usar a UIStackViewabordagem.


1
Esta é a minha abordagem preferida também. É uma melhoria significativa em outras respostas aqui, pois não reduz a visualização para o tamanho zero. A abordagem do squash terá problemas com qualquer coisa, exceto pagamentos bastante triviais. Um exemplo disso é a grande lacuna em que o item oculto está nesta outra resposta . E você pode acabar com violações de restrição, conforme mencionado.
Benjohn

@DanielSchlaug Obrigado por apontar isso. Atualizei a licença para o MIT. No entanto, eu exijo um high-five mental toda vez que alguém implementa essa solução.
Albert Bori 15/01

4

A melhor prática é que, quando tudo tiver as restrições de layout corretas, adicione uma altura ou restrição, dependendo de como você deseja que as vistas ao redor movam e conectem a restrição a uma IBOutletpropriedade.

Verifique se suas propriedades estão strong

no código, basta definir a constante como 0 e ativá- la, ocultar o conteúdo ou desativá- lo para mostrar o conteúdo. Isso é melhor do que mexer com o valor constante e restaurá-lo. Não esqueça de ligar layoutIfNeededdepois.

Se o conteúdo a ser oculto estiver agrupado, a melhor prática é colocar tudo em uma visualização e adicionar restrições a essa visualização.

@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];
}

Depois de configurar, você pode testá-lo no IntefaceBuilder, definindo sua restrição como 0 e depois voltando ao valor original. Não se esqueça de verificar outras prioridades de restrições, para que, quando ocultas, não haja conflito algum. outra maneira de testá-lo é colocá-lo em 0 e definir a prioridade como 0, mas não esqueça de restaurá-lo para a prioridade mais alta novamente.


1
Isso ocorre porque a restrição e a visualização nem sempre podem ser mantidas pela hierarquia da visualização; portanto, se você desativar a restrição e ocultar a visualização, ainda as manterá para exibi-las novamente, independentemente do que sua hierarquia de visualização decidir manter ou não.
Jorge Arimany 30/05


2

Meu método preferido é muito semelhante ao sugerido por Jorge Arimany.

Eu prefiro criar várias restrições. Primeiro, crie suas restrições para quando o segundo rótulo estiver visível. Crie uma saída para a restrição entre o botão e o segundo rótulo (se você estiver usando objc, verifique se ele é forte). Essa restrição determina a altura entre o botão e o segundo rótulo quando estiver visível.

Em seguida, crie outra restrição que especifique a altura entre o botão e o rótulo superior quando o segundo botão estiver oculto. Crie uma tomada com a segunda restrição e verifique se essa tomada possui um forte ponteiro. Em seguida, desmarque ainstalled caixa de seleção no construtor de interface e verifique se a prioridade da primeira restrição é menor que a segunda prioridade de restrições.

Finalmente, quando você oculta o segundo rótulo, alterne o .isActive propriedade dessas restrições e chamesetNeedsDisplay()

E é isso, sem números mágicos, sem matemática, se você tiver várias restrições para ativar e desativar, você pode até usar coleções de tomadas para mantê-las organizadas por estado. (O AKA mantém todas as restrições ocultas em um OutletCollection e as não ocultas em outro e apenas itera sobre cada coleção, alternando seu status .isActive).

Eu sei que Ryan Romanchuk disse que não queria usar várias restrições, mas acho que isso não é microgerenciamento e é mais simples que criar dinamicamente visualizações e restrições programaticamente (que é o que eu acho que ele queria evitar se eu estou lendo a pergunta corretamente).

Eu criei um exemplo simples, espero que seja útil ...

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

insira a descrição da imagem aqui


0

Também fornecerei minha solução para oferecer variedade.) Acho que a criação de uma saída para a largura / altura de cada item mais o espaçamento é ridícula e explode o código, os possíveis erros e o número de complicações.

Meu método remove todas as visualizações (no meu caso, instâncias UIImageView), seleciona quais precisam ser adicionadas novamente e, em um loop, adiciona novamente cada uma e cria novas restrições. É realmente muito simples, por favor, siga adiante. Aqui está o meu código rápido e sujo para fazer isso:

// 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);;
}];

Recebo um layout e espaçamento limpos e consistentes. Meu código usa Maçonaria, eu recomendo: https://github.com/SnapKit/Masonry


0

Experimente o BoxView , ele torna o layout dinâmico conciso e legível.
No seu caso, é:

    boxView.optItems = [
        firstLabel.boxed.useIf(isFirstLabelShown),
        secondLabel.boxed.useIf(isSecondLabelShown),
        button.boxed
    ]
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.