O que é NSLayoutConstraint "Altura do layout encapsulado em UIView" e como devo forçá-lo a recalcular de forma limpa?

262

Tenho uma UITableViewexecução no iOS 8 e estou usando alturas de células automáticas de restrições em um storyboard.

Uma das minhas células contém uma única UITextViewe eu preciso que ela se contraia e expanda com base na entrada do usuário - toque para diminuir / expandir o texto.

Estou fazendo isso adicionando uma restrição de tempo de execução à exibição de texto e alterando a constante na restrição em resposta a eventos do usuário:

-(void)collapse:(BOOL)collapse; {

    _collapsed = collapse;

    if(collapse)
        [_collapsedtextHeightConstraint setConstant: kCollapsedHeight]; // 70.0
    else
        [_collapsedtextHeightConstraint setConstant: [self idealCellHeightToShowFullText]];

    [self setNeedsUpdateConstraints];

}

Sempre que faço isso, envolvo-o em tableViewatualizações e chamo [tableView setNeedsUpdateConstraints]:

[tableView beginUpdates];

[_briefCell collapse:!_showFullBriefText];

[tableView setNeedsUpdateConstraints];
// I have also tried 
// [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
// with exactly the same results.

[tableView endUpdates];

Quando faço isso, meu celular se expande (e anima enquanto o faz), mas recebo um aviso de restrições:

2014-07-31 13:29:51.792 OneFlatEarth[5505:730175] Unable to simultaneously satisfy constraints.

Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 

(

    "<NSLayoutConstraint:0x7f94dced2b60 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...'(388)]>",

    "<NSLayoutConstraint:0x7f94dced2260 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...']-(15)-|   (Names: '|':UITableViewCellContentView:0x7f94de5773a0 )>",

    "<NSLayoutConstraint:0x7f94dced2350 V:|-(6)-[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...']   (Names: '|':UITableViewCellContentView:0x7f94de5773a0 )>",

    "<NSLayoutConstraint:0x7f94dced6480 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7f94de5773a0(91)]>"
 )

Will attempt to recover by breaking constraint 

<NSLayoutConstraint:0x7f94dced2b60 V:[UITextView:0x7f94d9b2b200'Brief text: Lorem Ipsum i...'(388)]>

388 é minha altura calculada, as outras restrições UITextViewsão as de Xcode / IB.

O último está me incomodando - acho que essa UIView-Encapsulated-Layout-Heighté a altura calculada da célula quando ela é renderizada pela primeira vez - (defino minha UITextViewaltura como> = 70.0), mas não parece certo que essa restrição derivada substitua uma usuário atualizado cnstraint.

Pior, embora o código de layout diga que está tentando quebrar minha restrição de altura, isso não acontece - ele recalcula a altura da célula e tudo é desenhado como eu gostaria.

Então, o que é NSLayoutConstraint UIView-Encapsulated-Layout-Height(suponho que seja a altura calculada para o dimensionamento automático de células) e como devo forçá-lo a recalcular corretamente?

Rog
fonte
3
cruz postada em fóruns dev Apple: devforums.apple.com/thread/238803
Rog
3
Resolvi um problema semelhante da seguinte maneira e funciona no iOS 7/8. 1) Abaixe uma das prioridades de restrição para 750. Eu tentaria o 1º ou o 2º 2) Na subclasse da célula no conjunto awakeFromNib self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;. Eu acho que a configuração inicial da máscara de redimensionamento automático impede que a última restrição seja adicionada. Eu encontrei esta solução aqui: github.com/wordpress-mobile/WordPress-iOS/commit/…
Jesse
3
@RogerNolan, alguma notícia? Encontrei o mesmo problema ao jogar com o layout automático no Interface Builder. Algumas células causam esse problema, outras não.
Orkenstein
3
Não acho que adicionar uma marca de redimensionamento automático seja uma boa solução.
Rog
1
@RogerNolan, você está gerando essa célula no IB antes de realizar essas alterações no layout? Estou depurando o mesmo problema, mas não estava adicionando restrições extras. Consegui suprimir o aviso reconstruindo minha visão do zero e, quando diferenciei os dois arquivos de storyboard, a única diferença foi que a versão com os avisos estava faltando a linha <rect key="frame" x="0.0" y="0.0" width="600" height="110"/>em sua definição, levando-me a acreditar que este é um bug do IB . Pelo menos o meu era assim mesmo.
Ell Neal

Respostas:

301

Tente diminuir a prioridade do seu _collapsedtextHeightConstraintpara 999. Dessa forma, a UIView-Encapsulated-Layout-Heightrestrição fornecida pelo sistema sempre tem precedência.

É baseado no que você retorna -tableView:heightForRowAtIndexPath:. Certifique-se de retornar o valor certo, sua própria restrição e a gerada deve ser a mesma. A prioridade mais baixa para sua própria restrição é necessária apenas temporariamente para evitar conflitos enquanto as animações de recolhimento / expansão estão em andamento.

Ortwin Gentz
fonte
74
Isso atingiria exatamente o oposto do que eu quero. O UIView-Encapsulated-Layout-Height está incorreto - ele pertence ao layout anterior.
Rog
7
A UIView-Encapsulated-Layout-Heightrestrição é adicionada pelo UITableView depois que as alturas são determinadas. Calculo a altura com base no systemLayoutSizeFittingSizecontentView. Aqui, UIView-Encapsulated-Layout-Heightnão importa. Em seguida, o tableView define o contentSize explicitamente para o valor retornado por heightForRowAtIndexPath:. Nesse caso, é correto diminuir a prioridade de nossas restrições personalizadas, porque a restrição tableView deve ter precedência após o cálculo do rowHeights.
Ortwin Gentz ​​16/09
8
@OrtwinGentz: Ainda não entendi o que é correto diminuir a prioridade de nossas restrições personalizadas, porque a restrição tableView deve ter precedência após o cálculo do rowHeights . O problema é que UIView-Encapsulated-Layout-Heightse eu não abaixar a prioridade ...
testando
38
Eu mantenho que evitar conflitos não resolve o problema real - embora, como ainda pareça um bug da Apple, acho que é provavelmente a coisa certa a fazer. Especialmente devido ao fato de a Apple recalcular essa restrição e tudo aparecer corretamente após a impressão do erro.
Rog
9
-1 para esta resposta, pois assume que a restrição que está sendo adicionada está correta. O adicionado UIView-Encapsulated-Layout-Widthno meu caso está errado, mas parece ser preferível às minhas restrições explícitas em tempo de execução.
ray
68

Eu tenho um cenário semelhante: uma exibição de tabela com uma célula de linha, na qual existem algumas linhas de objetos UILabel. Estou usando o iOS 8 e o autolayout.

Quando eu girei, obtive a altura calculada da linha do sistema incorreta (43,5 é muito menor que a altura real). Parece que:

"<NSLayoutConstraint:0x7bc2b2c0 'UIView-Encapsulated-Layout-Height' V:[UITableViewCellContentView:0x7bc37f30(43.5)]>"

Não é apenas um aviso. O layout da minha célula de exibição de tabela é terrível - todo o texto se sobrepõe em uma linha de texto.

Surpreende-me que a seguinte linha "corrija" meu problema magicamente (o autolayout não reclama nada e recebo o que espero na tela):

myTableView.estimatedRowHeight = 2.0; // any number but 2.0 is the smallest one that works

com ou sem esta linha:

myTableView.rowHeight = UITableViewAutomaticDimension; // by itself this line only doesn't help fix my specific problem
Thumb dourado
fonte
8
Finalmente! Esta é uma resposta correta. Na sessão da WWDC, foi mencionado que, se você usar o dimensionamento automático da altura da linha, deverá definir um valor estimado de RowHeight ou acontecer coisas ruins. (Sim, coisas realmente desagradáveis ​​da Apple aconteceram)
Abdalrahman Shatou
50
FWIW, adicionar a estimativa não fez nenhuma diferença para mim.
Benjohn
3
Heh. Estou de volta a essa mesma resposta novamente e fui implementá-la com alegria e esperança. Mais uma vez, ele não fez nenhuma diferença para mim :-)
Benjohn
3
Estimativas retornadas por tableView: estimadoHeightForRowAtIndexPath: DEVE ser pelo menos tão grande quanto a célula. Caso contrário, a altura calculada da tabela será menor que a altura real, e a tabela poderá rolar para cima (por exemplo, depois que o desenrolar segue retornar à tabela). UITableViewAutomaticDimension não deve ser usado.
Matt
1
Observe que quanto menor, estimatedRowHeightmais frequentemente cellForRowAtIndexPathserá chamado inicialmente. altura da visualização da tabela dividida por vezes estimadoRowHeight, para ser exato. Em um iPad Pro de 12 polegadas, isso pode ser um número na casa dos milhares e martelará a fonte de dados e poderá resultar em um atraso significativo.
Mojo66
32

Consegui que o aviso fosse embora especificando uma prioridade em um dos valores na restrição que as mensagens de aviso dizem que precisavam ser quebradas (abaixo "Will attempt to recover by breaking constraint"). Parece que, desde que eu defina a prioridade como algo maior que 49, o aviso desaparecerá.

Para mim, isso significava mudar minha restrição, o aviso dizia que tentava quebrar:

@"V:|[contentLabel]-[quoteeLabel]|"

para:

@"V:|-0@500-[contentLabel]-[quoteeLabel]|"

De fato, posso adicionar uma prioridade a qualquer um dos elementos dessa restrição e ela funcionará. Não parece importar qual. Minhas células terminam na altura adequada e o aviso não é exibido. Roger, por exemplo, tente adicionar @500logo após a 388restrição do valor da altura (por exemplo 388@500).

Não sei ao certo por que isso funciona, mas investiguei um pouco. Na enumeração NSLayoutPriority , parece que o NSLayoutPriorityFittingSizeCompressionnível de prioridade é 50. A documentação para esse nível de prioridade diz:

Quando você envia uma mensagem de sizeSize para uma visualização, é calculado o menor tamanho grande o suficiente para o conteúdo da visualização. Este é o nível de prioridade com o qual a visualização deseja ser a menor possível nessa computação. Está bem baixo. Geralmente, não é apropriado fazer uma restrição exatamente nessa prioridade. Você quer ser maior ou menor.

A documentação para a fittingSizemensagem referenciada lê:

O tamanho mínimo da vista que satisfaz as restrições que possui. (somente leitura)

O AppKit define essa propriedade para o melhor tamanho disponível para a exibição, considerando todas as restrições que ela e suas subvisões mantêm e satisfazendo uma preferência para tornar a exibição o menor possível. Os valores de tamanho nesta propriedade nunca são negativos.

Eu não procurei além disso, mas parece fazer sentido que isso tenha algo a ver com o problema.

Jeff Bowen
fonte
Thatks Jeff. Ainda parece um bug para mim. A Apple não responsed ao rdar embora :-(
Rog
13

99,9% do tempo, ao usar células ou cabeçalhos personalizados, todos os conflitos UITableViewsocorrem quando a tabela é carregada pela primeira vez. Uma vez carregado, você normalmente não verá o conflito novamente.

Isso acontece porque a maioria dos desenvolvedores geralmente usa uma altura fixa ou restrição de ancoragem de algum tipo para criar um layout no elemento / célula / cabeçalho. O conflito ocorre porque, quando as UITableViewprimeiras cargas / sendo dispostas, ele define a altura de suas células como 0. Isso obviamente entra em conflito com suas próprias restrições. Para resolver isso, basta definir qualquer restrição de altura fixa para uma prioridade mais baixa ( .defaultHigh). Leia atentamente a mensagem do console e veja qual restrição o sistema de layout decidiu quebrar. Geralmente é esse que precisa de sua prioridade alterada. Você pode alterar a prioridade assim:

let companyNameTopConstraint = companyNameLabel.topAnchor.constraint(equalTo: companyImageView.bottomAnchor, constant: 15)
    companyNameTopConstraint.priority = .defaultHigh

NSLayoutConstraint.activate([
            companyNameTopConstraint,
           the rest of your constraints here
            ])
UMAD
fonte
1
Linda explicação.
Glenn
12

Eu era capaz de resolver esse erro removendo uma espúria cell.layoutIfNeeded()que eu tive na minha tableView's cellForRowAtmétodo.

Clifton Labrum
fonte
1
Sim, isso também resolveu o problema para mim. Eu estava fazendo restrições de layout de código, então pensei inicialmente que poderia perder alguma coisa. Obrigado
John
1
A mesma coisa! Obrigado!
Andrey Chernukha
7

Em vez de informar a exibição da tabela para atualizar suas restrições, tente recarregar a célula:

[tableView beginUpdates];

[_briefCell collapse:!_showFullBriefText];

[tableView reloadRowsAtIndexPaths:@[[tableView indexPathForCell:_briefCell]] withRowAnimation:UITableViewRowAnimationNone];

[tableView endUpdates];

UIView-Encapsulated-Layout-Height é provavelmente a altura que a visualização da tabela calculou para a célula durante o carregamento inicial, com base nas restrições da célula naquele momento.

Austin
fonte
Eu deveria ter declarado na minha pergunta, eu tentei isso e não funciona. Eu concordo com o seu palpite sobre UIView-Encapsulated-layout-Altura
Rog
6
Tenha a recompensa de qualquer maneira por pelo menos enviar uma resposta. Parece que SO deixará evaporar de outra forma.
Rog
6

Outra possibilidade:

Se você estiver usando o layout automático para calcular a altura da célula (altura do contentView, na maioria das vezes como abaixo), e se tiver um separador de visualização uitabl, precisará adicionar a altura do separador para retornar a altura da célula. Depois de obter a altura correta, você não receberá o aviso de pagamento automático.

- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
   [sizingCell setNeedsLayout];
   [sizingCell layoutIfNeeded];
   CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
   return size.height; // should + 1 here if my uitableviewseparatorstyle is not none

}
Cullen SUN
fonte
Isso me ajudou em um caso em que obtive ambiguidades de altura de layout em um layout de célula bastante complexo apenas em alguns simuladores (iPad 6 Plus). Parece-me que, devido a alguns erros internos de arredondamento, o conteúdo é um pouco comprimido e, se as restrições não estão preparadas para serem compactadas, eu tenho ambiguidades. Então, ao invés de retornar UITableViewAutomaticDimensionem heightForRowAtIndexPathI voltar [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize] + 0.1
Leo
Quero dizer, é claro[sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 0.1
Leo
Incrivelmente; remover o separador é o que fez minha mesa se comportar, então obrigado.
royalmurder
5

Como mencionado por Jesse no comentário da pergunta, isso funciona para mim:

self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

Para sua informação, esse problema não ocorre no iOS 10.

Phu Nguyen
fonte
2
Em Swift 4.2: self.contentView.autoresizingMask = [.flexibleHeight]
airowe
4

Eu tive esse erro ao usar UITableViewAutomaticDimension e alterar uma restrição de altura em uma exibição dentro da célula.

Finalmente descobri que era devido ao valor constante da restrição não ser arredondado para o número inteiro mais próximo.

let neededHeight = width / ratio // This is a CGFloat like 133.2353
constraintPictureHeight.constant = neededHeight // Causes constraint error
constraintPictureHeight.constant = ceil(neededHeight) // All good!
Che
fonte
2
Este foi o comentário que me deu uma pista do meu problema. Eu tenho uma célula de imagem que carrega dinamicamente (cresce e diminui) e uma tableview com estimadoRowHeight = 50 e rowHeight = UITableViewAutomaticDimension. Eu ainda estava quebrando as restrições, mesmo que a tableview tivesse a altura correta. Acontece que os separadores tinham 0,3333 de altura e foi isso que estava chutando minha restrição de tamanho de imagem na célula a ser quebrada. Depois de desligar os separadores, tudo estava bem. Obrigado Che por me dar o que procurar.
precisa saber é o seguinte
1
Nesse caso, crie uma restrição extra, por exemplo, bottomMargin> = view.bottomMargin+1@900. O AutoLayout tenta acomodar 1 ponto extra, redimensiona a célula, fica confuso por causa da altura do separador, tenta quebrar / relaxar algumas restrições, encontra o @ 900 e descarta isso. Você obtém o layout desejado sem avisos.
Anton
salve o meu dia. algumas horas de qualquer maneira
Anton Tropashko 13/03/19
1

O dimensionamento da exibição de texto para ajustar seu conteúdo e a atualização da constante de restrição de altura para a altura resultante corrigiram o UIView-Encapsulated-Layout-Heightconflito de restrições para mim, por exemplo:

[self.textView sizeToFit];
self.textViewHeightConstraint.constant = self.textView.frame.size.height;
Kim André Sand
fonte
Onde você fez isso? No layoutSubviews?
stuckj
1

Depois de passar algumas horas coçando a cabeça com esse bug, finalmente encontrei uma solução que funcionou para mim. meu principal problema era que eu tinha várias pontas registradas para diferentes tipos de célula, mas um tipo de célula especificamente tinha tamanhos diferentes (nem todas as instâncias dessa célula terão o mesmo tamanho). então o problema surgiu quando a tableview estava tentando desenfileirar uma célula desse tipo e passou a ter uma altura diferente. Eu o resolvi definindo

self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; self.frame = CGRectMake(0, 0, self.frame.size.width, {correct height});

sempre que a célula tiver seus dados para calcular seu tamanho. Eu acho que pode estar em

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

algo como

cell.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; cell.frame = CGRectMake(0, 0, self.frame.size.width, {correct height});

Espero que isto ajude!

Alcides Eduardo Zelaya
fonte
0

O TableView obtém a altura da célula no indexPath do delegado. em seguida, obtenha o celular de cellForRowAtIndexPath:

top (10@1000)
    cell
bottom (0@1000)

if cell.contentView.height: 0 // <-> (UIView-Encapsulated-Layout-Height: 0 @ 1000) top (10 @ 1000) em conflito com (UIView-Encapsulated-Layout-Height: 0 @ 1000),

por causa de suas prioridades, é igual a 1000. Precisamos definir a prioridade máxima sob UIView-Encapsulated-Layout-Heighta prioridade.

user2962814
fonte
0

Eu estava recebendo uma mensagem como esta:

Não é possível satisfazer simultaneamente as restrições ...
...
...
...
NSLayoutConstraint: 0x7fe74bdf7e50 'Altura do layout encapsulado em UIView' V: [UITableViewCellContentView: 0x7fe75330c5c0 (21.5)]
...
... Tentará
recuperar por quebra de restrição NSLayoutConstraint: 0x7fe0f9b200c0 UITableViewCellContentView: 0x7fe0f9b1e090.bottomMargin == UILabel: 0x7fe0f9b1e970.bottom

Estou usando um costume UITableViewCellcom UITableViewAutomaticDimensionpara a altura. E eu também implementei o estimatedHeightForRowAtIndex:método.

A restrição que estava me dando problemas era algo como isto

[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[title]-|" options:0 metrics:nil views:views];

Alterar a restrição para isso resolverá o problema, mas, como outra resposta, achei que isso não estava correto, pois diminui a prioridade de uma restrição que eu quero que seja exigida:

[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-6@999-[title]-6@999-|" options:0 metrics:nil views:views];

No entanto, o que notei é que, se eu realmente remover a prioridade, isso também funcionará e não recebo os logs de restrição de quebra:

[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-6-[title]-6-|" options:0 metrics:nil views:views];

Isso é um pouco misterioso quanto à diferença entre |-6-[title]-6-|e |-[title-|. Mas especificar o tamanho não é um problema para mim e ele se livra dos logs, e não preciso diminuir a prioridade das minhas restrições necessárias.

user1687195
fonte
0

Defina isso view.translatesAutoresizingMaskIntoConstraints = NO;deve resolver esse problema.

kathy zhou
fonte
Isso funcionou perfeitamente para mim, mesmo que não fosse a solução perfeita.
Enkha 12/07/19
0

Eu tive um problema semelhante com uma célula de exibição de coleção.

Eu o resolvi abaixando a prioridade da restrição final que estava vinculada à parte inferior da célula (a última da cadeia de cima para baixo da vista - é isso que determina sua altura) para 999.

A altura da cela estava correta e os avisos desapareceram.

Rico
fonte