Quelle est la relation entre setNeedsLayout, layoutIfNeeded et layoutSubviews d'UIView?

105

Quelqu'un peut -il donner une explication définitive sur la relation entre UIView's setNeedsLayout, layoutIfNeededet les layoutSubviewsméthodes? Et un exemple d'implémentation où les trois seraient utilisés. Merci.

Ce qui me setNeedsLayoutrend confus, c'est que si j'envoie un message à ma vue personnalisée , la prochaine chose qu'elle invoque après cette méthode est de layoutSubviewssauter directement layoutIfNeeded. D'après les documents, je m'attendrais à ce que le flux soit setNeedsLayout> causes layoutIfNeededà appeler> causes layoutSubviewsà appeler.

Tarfa
la source

Réponses:

104

J'essaie toujours de comprendre cela moi-même, alors prenez cela avec scepticisme et pardonnez-moi s'il contient des erreurs.

setNeedsLayoutest simple: il place juste un drapeau quelque part dans l'UIView qui le marque comme nécessitant une mise en page. Cela forcera layoutSubviewsà être appelé sur la vue avant que le prochain dessin ne se produise. Notez que dans de nombreux cas, vous n'avez pas besoin d'appeler cela explicitement, à cause de la autoresizesSubviewspropriété. Si cela est défini (ce qui est le cas par défaut), toute modification apportée au cadre d'une vue entraînera la disposition de ses sous-vues.

layoutSubviewsest la méthode dans laquelle vous faites toutes les choses intéressantes. C'est l'équivalent de drawRectpour la mise en page, si vous voulez. Un exemple trivial pourrait être:

-(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 layoutIfNeededn'est généralement pas censé être remplacé dans votre sous-classe. C'est une méthode que vous êtes censé appeler lorsque vous souhaitez qu'une vue soit mise en page maintenant . L'implémentation d'Apple pourrait ressembler à ceci:

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

Vous feriez appel layoutIfNeededà une vue pour la forcer (et ses super-vues si nécessaire) à être disposée immédiatement.

n8gray
la source
2
Merci - heureux que quelqu'un ait enfin répondu à cela. En attendant, j'ai également dû me plonger dans setNeedsDisplay - ce qui provoque l'appel de drawRect - et contentMode. Seul contentMode = UIViewContentModeRedraw provoquera l'appel de setNeedsDisplay lorsque les limites d'une vue changent. Les autres choix de contentMode entraînent uniquement la mise à l'échelle, le décalage ou l'alignement de la vue sur un bord. Mon UITableViewCell personnalisé ne voulait pas redessiner sur les changements d'orientation, il serait uniquement mis à l'échelle, jusqu'à ce que je définisse contentMode sur UIViewContentModeRedraw.
Tarfa
Je pense que peut-être le [self.subview1 layoutSubviews] dans votre code devrait être remplacé par [self.subview1 setNeedsLayout]. Puisque layoutSubviews est censé être remplacé mais n'est pas censé être appelé depuis ou vers une autre vue. Soit le framework détermine quand l'appeler (en regroupant plusieurs demandes de mise en page en un seul appel à l'efficacité), soit vous le faites indirectement en appelant layoutIfNeeded quelque part dans la hiérarchie de la vue.
Tarfa
Correction à mon commentaire ci-dessus: "... Puisque layoutSubviews est censé être remplacé ou appelé de soi-même mais n'est pas destiné à être appelé depuis ou vers une autre vue ..."
Tarfa
Je n'en suis vraiment pas sûr [subview1 layoutSubviews]. Il se pourrait très bien que drawRectvous ne soyez pas censé l'appeler directement. Mais je ne sais pas si un appel setNeedsLayoutpendant la phase de mise en page entraînera la mise en page de la vue au cours de la même phase de mise en page ou si elle est retardée jusqu'à la suivante. Ce serait bien de voir une réponse de quelqu'un qui comprend vraiment comment tout cela fonctionne ...
n8gray
34

Je voudrais ajouter la réponse de n8gray que dans certains cas, vous devrez appeler setNeedsLayoutsuivi de layoutIfNeeded.

Disons par exemple que vous avez écrit une vue personnalisée étendant UIView, dans laquelle le positionnement des sous-vues est complexe et ne peut pas être fait avec autoresizingMask ou iOS6 AutoLayout. Le positionnement personnalisé peut être effectué par substitution layoutSubviews.

À titre d'exemple, disons que vous avez une vue personnalisée qui a une contentViewpropriété et une edgeInsetspropriété qui permet de définir les marges autour du contentView. layoutSubviewsressemblerait à ceci:

- (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 vous souhaitez pouvoir animer le changement de cadre chaque fois que vous modifiez la edgeInsetspropriété, vous devez remplacer le edgeInsetssetter comme suit et appeler setNeedsLayoutsuivi 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 cette façon, si vous procédez comme suit, si vous modifiez la propriété edgeInsets à l'intérieur d'un bloc d'animation, le changement d'image de contentView sera animé.

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

Si vous n'ajoutez pas l'appel à layoutIfNeeded dans la méthode setEdgeInsets, l'animation ne fonctionnera pas car la layoutSubviews sera appelée au prochain cycle de mise à jour, ce qui revient à l'appeler en dehors du bloc d'animation.

Si vous appelez uniquement layoutIfNeeded dans la méthode setEdgeInsets, rien ne se passera car l'indicateur setNeedsLayout n'est pas défini.

quentinadam
la source
4
Ou vous devriez simplement pouvoir appeler [self layoutIfNeeded]dans le bloc d'animation après avoir défini les encarts de bord.
Scott Fister