Welche Beziehung besteht zwischen UIViews setNeedsLayout, layoutIfNeeded und layoutSubviews?

105

Kann mir jemand eine definitive Erklärung über die Beziehung zwischen geben UIView's setNeedsLayout, layoutIfNeededund layoutSubviewsMethoden? Und eine Beispielimplementierung, bei der alle drei verwendet würden. Vielen Dank.

Was mich verwirrt, ist, dass, wenn ich meiner benutzerdefinierten Ansicht eine setNeedsLayoutNachricht sende , das nächste, was sie nach dieser Methode aufruft layoutSubviews, das Überspringen ist layoutIfNeeded. Von den Dokumenten würde ich erwarten, dass der Fluss setNeedsLayout> Ursachen ist layoutIfNeeded, die aufgerufen werden> Ursachen layoutSubviews, die aufgerufen werden.

Tarfa
quelle

Antworten:

104

Ich versuche immer noch, das selbst herauszufinden, also nimm das mit einiger Skepsis und vergib mir, wenn es Fehler enthält.

setNeedsLayoutist einfach: Es setzt einfach irgendwo in der UIView ein Flag, das es als Layout erforderlich markiert. Dadurch muss layoutSubviewsdie Ansicht aufgerufen werden, bevor das nächste Neuzeichnen erfolgt. Beachten Sie, dass Sie dies in vielen Fällen aufgrund der autoresizesSubviewsEigenschaft nicht explizit aufrufen müssen . Wenn dies festgelegt ist (was standardmäßig der Fall ist), führt jede Änderung am Rahmen einer Ansicht dazu, dass die Ansicht ihre Unteransichten auslegt.

layoutSubviewsist die Methode, mit der Sie all die interessanten Dinge tun. drawRectWenn Sie so wollen, entspricht dies dem Layout. Ein triviales Beispiel könnte sein:

-(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 layoutIfNeededsoll in Ihrer Unterklasse im Allgemeinen nicht überschrieben werden. Diese Methode sollten Sie aufrufen, wenn Sie jetzt eine Ansicht erstellen möchten . Die Implementierung von Apple könnte ungefähr so ​​aussehen:

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

Sie würden layoutIfNeededeine Ansicht aufrufen, um zu erzwingen, dass sie (und gegebenenfalls ihre Übersichten) sofort angelegt werden.

n8gray
quelle
2
Danke - froh, dass endlich jemand darauf geantwortet hat. In der Zwischenzeit musste ich mich auch mit setNeedsDisplay - was dazu führt, dass drawRect aufgerufen wird - und contentMode befassen. Nur contentMode = UIViewContentModeRedraw bewirkt, dass setNeedsDisplay aufgerufen wird, wenn sich die Grenzen einer Ansicht ändern. Die anderen contentMode-Optionen bewirken nur, dass die Ansicht skaliert, um einen bestimmten Betrag verschoben oder an einer Kante ausgerichtet wird. Meine benutzerdefinierte UITableViewCell wollte bei Änderungen der Ausrichtung nicht neu zeichnen, sondern nur skalieren, bis ich contentMode auf UIViewContentModeRedraw gesetzt habe.
Tarfa
Ich denke, vielleicht sollte das [self.subview1 layoutSubviews] in Ihrem Code durch [self.subview1 setNeedsLayout] ersetzt werden. Da layoutSubviews überschrieben werden soll, aber nicht von oder zu einer anderen Ansicht aufgerufen werden soll. Entweder bestimmt das Framework, wann es aufgerufen werden soll (indem mehrere Layoutanforderungen aus Effizienzgründen zu einem Aufruf zusammengefasst werden), oder Sie rufen indirekt layoutIfNeeded irgendwo in der Ansichtshierarchie auf.
Tarfa
Korrektur zu meinem obigen Kommentar: "... Da layoutSubviews überschrieben oder von sich selbst aufgerufen werden soll, aber nicht von oder zu einer anderen Ansicht aufgerufen werden soll ..."
Tarfa
Ich bin mir wirklich nicht sicher [subview1 layoutSubviews]. Es könnte sehr gut sein, dass drawRectSie es nicht direkt anrufen sollten. Ich bin mir jedoch nicht sicher, ob ein Aufruf setNeedsLayoutwährend der Layoutphase dazu führt, dass die Ansicht während derselben Layoutphase angelegt wird oder ob sie sich bis zur nächsten verzögert. Es wäre schön, eine Antwort von jemandem zu sehen, der wirklich versteht, wie das alles funktioniert ...
n8gray
34

Ich möchte die Antwort von n8gray ergänzen, die Sie in einigen Fällen anrufen müssen, setNeedsLayoutgefolgt von layoutIfNeeded.

Angenommen, Sie haben eine benutzerdefinierte Ansicht geschrieben, die UIView erweitert. Die Positionierung von Unteransichten ist komplex und kann nicht mit autoresizingMask oder iOS6 AutoLayout durchgeführt werden. Die benutzerdefinierte Positionierung kann durch Überschreiben erfolgen layoutSubviews.

Angenommen , Sie haben eine benutzerdefinierte Ansicht mit einer contentViewEigenschaft und einer edgeInsetsEigenschaft, mit der die Ränder um die contentView festgelegt werden können. layoutSubviewswürde so aussehen:

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

Wenn Sie in der Lage sein möchten, die Rahmenänderung zu animieren, wenn Sie die edgeInsetsEigenschaft ändern , müssen Sie den edgeInsetsSetter wie folgt überschreiben und aufrufen, setNeedsLayoutgefolgt von 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
}

Wenn Sie auf diese Weise Folgendes tun und die Eigenschaft edgeInsets in einem Animationsblock ändern, wird die Rahmenänderung der contentView animiert.

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

Wenn Sie den Aufruf von layoutIfNeeded nicht in der setEdgeInsets-Methode hinzufügen, funktioniert die Animation nicht, da die layoutSubviews beim nächsten Aktualisierungszyklus aufgerufen werden, was einem Aufruf außerhalb des Animationsblocks entspricht.

Wenn Sie layoutIfNeeded nur in der Methode setEdgeInsets aufrufen, geschieht nichts, da das Flag setNeedsLayout nicht gesetzt ist.

Quentinadam
quelle
4
Oder Sie sollten nur in der Lage sein, [self layoutIfNeeded]innerhalb des Animationsblocks aufzurufen , nachdem Sie die Kanteneinsätze festgelegt haben.
Scott Fister