Qual é a relação entre setNeedsLayout, layoutIfNeeded e layoutSubviews do UIView?

105

Alguém pode dar uma explicação definitiva sobre a relação entre UIView's setNeedsLayout, layoutIfNeedede layoutSubviewsmétodos? E um exemplo de implementação onde todos os três seriam usados. Obrigado.

O que me deixa confuso é que, se eu enviar uma setNeedsLayoutmensagem para o meu modo de exibição personalizado, a próxima coisa que ele invoca após esse método é layoutSubviewspular imediatamente layoutIfNeeded. Pelos documentos, eu esperaria que o fluxo fosse setNeedsLayout> causas layoutIfNeededa serem chamadas> causas layoutSubviewsa serem chamadas.

Tarfa
fonte

Respostas:

104

Ainda estou tentando descobrir isso sozinho, então aceite isso com algum ceticismo e me perdoe se contém erros.

setNeedsLayouté fácil: ele apenas define um sinalizador em algum lugar do UIView que o marca como necessitando de layout. Isso forçará layoutSubviewsa chamada na visualização antes que o próximo redesenho aconteça. Observe que, em muitos casos, você não precisa chamar isso explicitamente, por causa da autoresizesSubviewspropriedade. Se estiver definido (o que é por padrão), qualquer alteração no quadro de uma visão fará com que a visão exiba suas subvisualizações.

layoutSubviewsé o método pelo qual você faz todas as coisas interessantes. É o equivalente drawRecta layout, se você quiser. Um exemplo trivial pode ser:

-(void)layoutSubviews {
    // Child's frame is always equal to our bounds inset by 8px
    self.subview1.frame = CGRectInset(self.bounds, 8.0, 8.0);
    // It seems likely that this is incorrect:
    // [self.subview1 layoutSubviews];
    // ... and this is correct:
    [self.subview1 setNeedsLayout];
    // but I don't claim to know definitively.
}

AFAIK layoutIfNeededgeralmente não deve ser sobrescrito em sua subclasse. É um método que você deve chamar quando quiser que uma visualização seja apresentada agora . A implementação da Apple pode ser mais ou menos assim:

-(void)layoutIfNeeded {
    if (self._needsLayout) {
        UIView *sv = self.superview;
        if (sv._needsLayout) {
            [sv layoutIfNeeded];
        } else {
            [self layoutSubviews];
        }
    }
}

Você chamaria layoutIfNeededuma visão para forçá-la (e suas supervisões, se necessário) a ser apresentada imediatamente.

n8gray
fonte
2
Obrigado - fico feliz que alguém finalmente respondeu isso. Nesse ínterim, também tive que me aprofundar em setNeedsDisplay - que faz com que drawRect seja chamado - e contentMode. Apenas contentMode = UIViewContentModeRedraw fará com que setNeedsDisplay seja chamado quando os limites de uma visualização forem alterados. As outras opções de contentMode apenas fazem com que a visualização seja dimensionada, deslocada em alguma quantidade ou alinhada a uma borda. Meu UITableViewCell personalizado não queria redesenhar as mudanças de orientação, ele apenas escalaria, até que eu definisse contentMode como UIViewContentModeRedraw.
Tarfa
Acho que talvez o [self.subview1 layoutSubviews] em seu código deva ser substituído por [self.subview1 setNeedsLayout]. Já que layoutSubviews deve ser sobrescrito, mas não deve ser chamado de ou para outra visão. A estrutura determina quando chamá-la (agrupando várias solicitações de layout em uma chamada de eficiência) ou indiretamente chamando layoutIfNeeded em algum lugar na hierarquia de visualização.
Tarfa
Correção para o meu comentário acima: "... Visto que layoutSubviews deve ser substituído ou chamado por si mesmo, mas não deve ser chamado de ou para outra visualização ..."
Tarfa
Eu realmente não tenho certeza sobre [subview1 layoutSubviews]. Pode muito bem ser que drawRectnão deva ligar diretamente. Mas não tenho certeza se a chamada setNeedsLayoutdurante a fase de layout fará com que a visualização seja exibida durante a mesma fase de layout ou se atrasar até a próxima. Seria bom ver uma resposta de alguém que realmente entende como tudo isso funciona ...
n8gray
34

Gostaria de acrescentar a resposta do n8gray que, em alguns casos, você precisará ligar setNeedsLayoutseguido por layoutIfNeeded.

Digamos, por exemplo, que você escreveu uma visualização customizada estendendo UIView, na qual o posicionamento de subvisualizações é complexo e não pode ser feito com autoresizingMask ou iOS6 AutoLayout. O posicionamento personalizado pode ser feito substituindo layoutSubviews.

Como exemplo, digamos que você tenha uma visualização personalizada que possui uma contentViewpropriedade e uma edgeInsetspropriedade que permite definir as margens ao redor de contentView. layoutSubviewsficaria assim:

- (void) layoutSubviews {
    self.contentView.frame = CGRectMake(
        self.bounds.origin.x + self.edgeInsets.left,
        self.bounds.origin.y + self.edgeInsets.top,
        self.bounds.size.width - self.edgeInsets.left - self.edgeInsets.right,
        self.bounds.size.height - self.edgeInsets.top - self.edgeInsets.bottom); 
}

Se você deseja poder animar a mudança de quadro sempre que alterar a edgeInsetspropriedade, você precisa substituir o configurador da edgeInsetsseguinte maneira e chamar setNeedsLayoutseguido por layoutIfNeeded:

- (void) setEdgeInsets:(UIEdgeInsets)edgeInsets {
    _edgeInsets = edgeInsets;
    [self setNeedsLayout]; //Indicates that the view needs to be laid out 
                           //at next update or at next call of layoutIfNeeded, 
                           //whichever comes first 
    [self layoutIfNeeded]; //Calls layoutSubviews if flag is set
}

Dessa forma, se você fizer o seguinte, se alterar a propriedade edgeInsets dentro de um bloco de animação, a mudança de quadro de contentView será animada.

[UIView animateWithDuration:2 animations:^{
    customView.edgeInsets = UIEdgeInsetsMake(45, 17, 18, 34);
}];

Se você não adicionar a chamada para layoutIfNeeded no método setEdgeInsets, a animação não funcionará porque o layoutSubviews será chamado no próximo ciclo de atualização, o que equivale a chamá-lo fora do bloco de animação.

Se você chamar layoutIfNeeded no método setEdgeInsets, nada acontecerá, pois o sinalizador setNeedsLayout não está definido.

quentinadam
fonte
4
Ou você deve apenas ser capaz de chamar [self layoutIfNeeded]dentro do bloco de animação depois de definir as inserções de borda.
Scott Fister