Qual è la relazione tra setNeedsLayout, layoutIfNeeded e layoutSubviews di UIView?


105

Qualcuno può dare una spiegazione definitiva sul rapporto tra UIView's setNeedsLayout, layoutIfNeedede layoutSubviewsmetodi? E un'implementazione di esempio in cui verranno utilizzati tutti e tre. Grazie.

Ciò che mi confonde è che se invio un setNeedsLayoutmessaggio alla mia visualizzazione personalizzata, la cosa successiva che invoca dopo questo metodo è layoutSubviewssaltare subito layoutIfNeeded. Dai documenti mi aspetto che il flusso sia setNeedsLayout> cause layoutIfNeededda chiamare> cause layoutSubviewsda chiamare.

Risposte:


104

Sto ancora cercando di capirlo da solo, quindi prendilo con un po 'di scetticismo e perdonami se contiene errori.

setNeedsLayoutè facile: imposta semplicemente una bandiera da qualche parte nella UIView che lo contrassegna come bisognoso di layout. Ciò costringerà layoutSubviewsa essere richiamato sulla visualizzazione prima che avvenga il prossimo ridisegno. Nota che in molti casi non è necessario chiamarlo esplicitamente, a causa della autoresizesSubviewsproprietà. Se è impostato (che è per impostazione predefinita), qualsiasi modifica alla cornice di una vista farà sì che la vista disponga le sue viste secondarie.

layoutSubviewsè il metodo in cui fai tutte le cose interessanti. È l'equivalente di drawRectper layout, se vuoi. Un esempio banale potrebbe essere:

-(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 layoutIfNeedednon è generalmente pensato per essere sovrascritto nella tua sottoclasse. È un metodo che dovresti chiamare quando vuoi che una vista sia disposta in questo momento . L'implementazione di Apple potrebbe essere simile a questa:

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

Si potrebbe chiamare layoutIfNeededil fine di forzarlo (e le sue superviews come necessario) da posare immediatamente.


2
Grazie - felice che qualcuno abbia finalmente risposto a questo. Nel frattempo ho anche dovuto approfondire setNeedsDisplay - che fa chiamare drawRect - e contentMode. Solo contentMode = UIViewContentModeRedraw farà sì che setNeedsDisplay venga chiamato quando i limiti di una vista cambiano. Le altre scelte di contentMode fanno sì che la vista venga ridimensionata, spostata di una certa quantità o allineata a un bordo. Il mio UITableViewCell personalizzato non voleva ridisegnare le modifiche all'orientamento, si ridimensionava solo fino a quando non ho impostato contentMode su UIViewContentModeRedraw.
Tarfa

Penso che forse [self.subview1 layoutSubviews] nel codice dovrebbe essere sostituito con [self.subview1 setNeedsLayout]. Poiché layoutSubviews è pensato per essere sovrascritto ma non è pensato per essere chiamato da o verso un'altra visualizzazione. O il framework determina quando chiamarlo (raggruppando più richieste di layout in una chiamata per l'efficienza) o lo fai indirettamente chiamando layoutIfNeeded da qualche parte nella gerarchia della vista.
Tarfa

Correzione al mio commento di cui sopra: "... Poiché layoutSubviews è pensato per essere sovrascritto o chiamato da sé ma non è pensato per essere chiamato da o verso un'altra vista ..."
Tarfa

Non ne sono davvero sicuro [subview1 layoutSubviews]. Potrebbe benissimo essere che, tipo drawRect, non dovresti chiamarlo direttamente. Ma non sono sicuro se la chiamata setNeedsLayoutdurante la fase di layout causerà il layout della vista durante la stessa fase di layout o se verrà ritardata fino a quella successiva. Sarebbe bello vedere una risposta da qualcuno che capisce davvero come funziona tutto questo ...
n8gray

34

Vorrei aggiungere la risposta di n8gray che in alcuni casi dovrai chiamare setNeedsLayoutseguito da layoutIfNeeded.

Supponiamo ad esempio che tu abbia scritto una visualizzazione personalizzata che estende UIView, in cui il posizionamento delle sottoview è complesso e non può essere eseguito con autoresizeMask o iOS6 AutoLayout. Il posizionamento personalizzato può essere eseguito sovrascrivendo layoutSubviews.

Ad esempio, supponiamo di avere una visualizzazione personalizzata che ha una contentViewproprietà e una edgeInsetsproprietà che consente di impostare i margini attorno a contentView. layoutSubviewssarebbe simile a questo:

- (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 vuoi essere in grado di animare il cambio di frame ogni volta che cambi la edgeInsetsproprietà, devi sovrascrivere il edgeInsetssetter come segue e chiamare setNeedsLayoutseguito da 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
}

In questo modo, se si esegue quanto segue, se si modifica la proprietà edgeInsets all'interno di un blocco di animazione, il cambio di frame di contentView verrà animato.

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

Se non aggiungi la chiamata a layoutIfNeeded nel metodo setEdgeInsets, l'animazione non funzionerà perché layoutSubviews verrà chiamato al successivo ciclo di aggiornamento, il che equivale a chiamarlo al di fuori del blocco dell'animazione.

Se chiami solo layoutIfNeeded nel metodo setEdgeInsets, non accadrà nulla poiché il flag setNeedsLayout non è impostato.


4
Oppure dovresti essere in grado di chiamare [self layoutIfNeeded]all'interno del blocco dell'animazione dopo aver impostato i margini.
Scott Fister
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.