web-dev-qa-db-fra.com

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

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

Ce qui me rend confus, c'est que si j'envoie à ma vue personnalisée un message setNeedsLayout, la prochaine chose qu'il invoque après cette méthode est layoutSubviews, en sautant directement sur layoutIfNeeded. D'après les documents, je m'attendrais à ce que le flux soit setNeedsLayout> provoque layoutIfNeeded à être appelé> provoque layoutSubviews à être appelé.

105
Tarfa

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

setNeedsLayout est simple: il définit simplement un indicateur quelque part dans l'UIView qui le marque comme ayant besoin d'une mise en page. Cela forcera layoutSubviews à être appelé dans la vue avant le prochain redessin. Notez que dans de nombreux cas, vous n'avez pas besoin d'appeler cela explicitement, en raison de la propriété autoresizesSubviews. Si ce paramètre est défini (ce qu'il est par défaut), toute modification du cadre d'une vue entraînera la présentation de ses sous-vues.

layoutSubviews est la méthode dans laquelle vous faites toutes les choses intéressantes. C'est l'équivalent de drawRect pour 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 layoutIfNeeded n'est généralement pas destiné à être remplacé dans votre sous-classe. C'est une méthode que vous êtes censé appeler lorsque vous souhaitez disposer une vue en ce moment. 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 appelleriez layoutIfNeeded sur une vue pour la forcer (et ses superviews si nécessaire) à être mis en page immédiatement.

103
n8gray

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

Supposons, par exemple, que vous ayez écrit une vue personnalisée étendant UIView, dans laquelle le positionnement des sous-vues est complexe et ne peut pas être effectué avec le redimensionnement automatiqueMask ou iOS6 AutoLayout. Le positionnement personnalisé peut être effectué en remplaçant layoutSubviews.

Par exemple, disons que vous avez une vue personnalisée qui a une propriété contentView et une propriété edgeInsets qui permet de définir les marges autour de contentView. layoutSubviews ressemblerait à 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 propriété edgeInsets, vous devez remplacer le setter edgeInsets comme suit et appeler setNeedsLayout suivi 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 les layoutSubviews seront appelés au prochain cycle de mise à jour, ce qui équivaut à 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.

34
quentinadam