¿Cuál es la relación entre setNeedsLayout, layoutIfNeeded y layoutSubviews de UIView?

105

¿Puede alguien dar una explicación definitiva sobre la relación entre UIView's setNeedsLayout, layoutIfNeededy layoutSubviewsmétodos? Y una implementación de ejemplo donde se usarían los tres. Gracias.

Lo que me confunde es que si envío un setNeedsLayoutmensaje a mi vista personalizada, lo siguiente que invoca después de este método es layoutSubviewssaltar a la derecha layoutIfNeeded. De los documentos, esperaría que el flujo sea setNeedsLayout> causas layoutIfNeededque se llamen> causas layoutSubviewsque se llamen.

Tarfa
fuente

Respuestas:

104

Todavía estoy tratando de resolver esto por mí mismo, así que tómate esto con cierto escepticismo y perdóname si contiene errores.

setNeedsLayoutes fácil: simplemente establece una bandera en algún lugar de la UIView que lo marca como que necesita diseño. Eso obligará layoutSubviewsa ser llamado en la vista antes de que ocurra el próximo redibujado. Tenga en cuenta que en muchos casos no es necesario llamar a esto explícitamente debido a la autoresizesSubviewspropiedad. Si está configurado (que es de forma predeterminada), cualquier cambio en el marco de una vista hará que la vista disponga sus subvistas.

layoutSubviewses el método con el que haces todas las cosas interesantes. Es el equivalente a drawRectpara el diseño, por así decirlo. Un ejemplo trivial podría 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 layoutIfNeededgeneralmente no está destinado a ser anulado en su subclase. Es un método al que debe llamar cuando desee que se presente una vista en este momento . La implementación de Apple podría verse así:

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

Se podría llamar layoutIfNeededen una vista a la fuerza (y sus superviews como sea necesario) para ser presentado inmediatamente.

n8gray
fuente
2
Gracias, me alegro de que alguien finalmente haya respondido a esto. Mientras tanto, también tuve que profundizar en setNeedsDisplay, que hace que se llame a drawRect, y contentMode. Solo contentMode = UIViewContentModeRedraw hará que se llame a setNeedsDisplay cuando cambien los límites de una vista. Las otras opciones de contentMode solo hacen que la vista se escale, se cambie en cierta medida o se alinee con un borde. Mi UITableViewCell personalizado no quería volver a dibujar en los cambios de orientación, solo escalaría, hasta que establezca contentMode en UIViewContentModeRedraw.
Tarfa
Creo que tal vez [self.subview1 layoutSubviews] en su código debería ser reemplazado por [self.subview1 setNeedsLayout]. Dado que layoutSubviews está destinado a ser anulado, pero no está destinado a ser llamado desde o hacia otra vista. O el marco determina cuándo llamarlo (agrupando varias solicitudes de diseño en una llamada para mayor eficiencia) o usted lo hace indirectamente llamando a layoutIfNeeded en algún lugar de la jerarquía de vistas.
Tarfa
Corrección a mi comentario anterior: "... Dado que layoutSubviews está destinado a ser anulado o llamado desde uno mismo, pero no está destinado a ser llamado desde o hacia otra vista ..."
Tarfa
Realmente no estoy seguro [subview1 layoutSubviews]. Bien podría ser que, como drawRect, se supone que no debes llamarlo directamente. Pero no estoy seguro de si llamar setNeedsLayoutdurante la fase de diseño hará que la vista se presente durante la misma fase de diseño o si se retrasa hasta la siguiente. Sería bueno ver una respuesta de alguien que realmente entienda cómo funciona todo esto ...
n8gray
34

Me gustaría agregar la respuesta de n8gray que en algunos casos necesitará llamar setNeedsLayoutseguido de layoutIfNeeded.

Digamos, por ejemplo, que escribió una vista personalizada ampliando UIView, en la que el posicionamiento de las subvistas es complejo y no se puede hacer con autoresizingMask o AutoLayout de iOS6. El posicionamiento personalizado se puede realizar anulando layoutSubviews.

Como ejemplo, digamos que tiene una vista personalizada que tiene una contentViewpropiedad y una edgeInsetspropiedad que permite establecer los márgenes alrededor de contentView. layoutSubviewsse vería así:

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

Si desea poder animar el cambio de marco siempre que cambie la edgeInsetspropiedad, debe anular el establecedor de la edgeInsetssiguiente manera y llamar setNeedsLayoutseguido de 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
}

De esa manera, si hace lo siguiente, si cambia la propiedad edgeInsets dentro de un bloque de animación, se animará el cambio de fotograma de contentView.

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

Si no agrega la llamada a layoutIfNeeded en el método setEdgeInsets, la animación no funcionará porque se llamará a layoutSubviews en el próximo ciclo de actualización, lo que equivale a llamarlo fuera del bloque de animación.

Si solo llama a layoutIfNeeded en el método setEdgeInsets, no sucederá nada ya que la marca setNeedsLayout no está establecida.

quentinadam
fuente
4
O simplemente debería poder llamar [self layoutIfNeeded]dentro del bloque de animación después de configurar las inserciones de borde.
Scott Fister