Jaka jest zależność między setNeedsLayout, layoutIfNeeded i layoutSubviews w UIView?

105

Czy ktoś może dać ostateczne wyjaśnienie relacji pomiędzy UIView's setNeedsLayout, layoutIfNeededi layoutSubviewsmetodami? I przykładowa implementacja, w której zostaną użyte wszystkie trzy. Dzięki.

Wprawia mnie w zakłopotanie, że jeśli wyślę setNeedsLayoutwiadomość do widoku niestandardowego, następną rzeczą, którą wywoła po tej metodzie, będzie layoutSubviewsprzeskakiwanie layoutIfNeeded. Z dokumentów, których spodziewałbym się, że przepływ będzie setNeedsLayout> powodami, layoutIfNeededktóre zostaną nazwane> przyczynami, layoutSubviewsktóre zostaną wywołane.

Tarfa
źródło

Odpowiedzi:

104

Nadal próbuję to rozgryźć, więc potraktuj to z pewnym sceptycyzmem i wybacz mi, jeśli zawiera błędy.

setNeedsLayoutjest łatwy: po prostu ustawia flagę gdzieś w UIView, która oznacza, że ​​wymaga układu. To wymusi layoutSubviewswywołanie widoku przed następnym przerysowaniem. Zauważ, że w wielu przypadkach nie musisz wywoływać tego jawnie ze względu na autoresizesSubviewswłaściwość. Jeśli tak jest (a jest to ustawienie domyślne), każda zmiana w ramce widoku spowoduje, że widok rozłoży podglądy.

layoutSubviewsto metoda, w której robisz wszystkie interesujące rzeczy. Jest to odpowiednik drawRectdla układu, jeśli wolisz. Trywialnym przykładem może być:

-(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 layoutIfNeedednie jest generalnie przeznaczony do zastąpienia w Twojej podklasie. Jest to metoda, że masz na myśli, aby zadzwonić, jeśli chcesz aby być rozplanowane teraz . Implementacja Apple może wyglądać mniej więcej tak:

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

Nazwałbyś layoutIfNeededna celu zmusić go (i jego superviews jak jest to konieczne), aby być rozplanowane natychmiast.

n8gray
źródło
2
Dziękuję - cieszę się, że ktoś w końcu odpowiedział. W międzyczasie musiałem też zagłębić się w setNeedsDisplay - co powoduje wywołanie drawRect - i contentMode. Tylko contentMode = UIViewContentModeRedraw spowoduje wywołanie setNeedsDisplay w przypadku zmiany granic widoku. Inne opcje contentMode powodują tylko skalowanie widoku, przesuwanie o pewną wartość lub wyrównanie do krawędzi. Mój niestandardowy UITableViewCell nie chciał przerysowywać po zmianach orientacji, tylko skalował się, dopóki nie ustawię contentMode na UIViewContentModeRedraw.
Tarfa
Myślę, że być może [self.subview1 layoutSubviews] w twoim kodzie powinno zostać zastąpione przez [self.subview1 setNeedsLayout]. Ponieważ layoutSubviews ma być nadpisany, ale nie ma być wywoływany z lub do innego widoku. Albo struktura określa, kiedy ją wywołać (przez zgrupowanie wielu żądań układu w jedno wywołanie w celu zwiększenia wydajności), albo wykonujesz to pośrednio, wywołując layoutIfNeeded gdzieś w hierarchii widoków.
Tarfa
Korekta mojego powyższego komentarza: "... Ponieważ layoutSubviews ma być nadpisany lub wywołany z siebie, ale nie ma być wywoływany z lub do innego widoku ..."
Tarfa
Naprawdę nie jestem pewien [subview1 layoutSubviews]. Bardzo dobrze może być tak drawRect, że nie powinieneś dzwonić do tego bezpośrednio. Ale nie jestem pewien, czy wywołanie setNeedsLayoutpodczas fazy układu spowoduje, że widok zostanie ułożony w tej samej fazie układu, czy też zostanie opóźniony do następnej. Byłoby miło zobaczyć odpowiedź od kogoś, kto naprawdę rozumie, jak to wszystko działa ...
n8gray
34

Chciałbym dodać do odpowiedzi n8gray, że w niektórych przypadkach będziesz musiał zadzwonić, setNeedsLayouta następnie layoutIfNeeded.

Załóżmy na przykład, że napisałeś niestandardowy widok rozszerzający UIView, w którym pozycjonowanie podglądów podrzędnych jest złożone i nie można tego zrobić za pomocą funkcji autoresizeMask lub AutoLayout iOS6. Niestandardowe pozycjonowanie można wykonać przez nadpisanie layoutSubviews.

Jako przykład załóżmy, że masz niestandardowy widok, który ma contentViewwłaściwość i edgeInsetswłaściwość, która umożliwia ustawienie marginesów wokół contentView. layoutSubviewswyglądałoby tak:

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

Jeśli chcesz mieć możliwość animowania zmiany klatki po każdej zmianie edgeInsetswłaściwości, musisz nadpisać edgeInsetsmetodę ustawiającą w następujący sposób i wywołać, setNeedsLayouta następnie 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
}

W ten sposób, jeśli wykonasz następujące czynności, jeśli zmienisz właściwość edgeInsets wewnątrz bloku animacji, animowana będzie zmiana klatki elementu contentView.

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

Jeśli nie dodasz wywołania do layoutIfNeeded w metodzie setEdgeInsets, animacja nie zadziała, ponieważ layoutSubviews zostanie wywołany w następnym cyklu aktualizacji, co jest równoznaczne z wywołaniem go poza blokiem animacji.

Jeśli wywołasz tylko layoutIfNeeded w metodzie setEdgeInsets, nic się nie stanie, ponieważ flaga setNeedsLayout nie jest ustawiona.

quentinadam
źródło
4
Lub powinieneś być w stanie wywołać [self layoutIfNeeded]w bloku animacji po ustawieniu wstawek krawędzi.
Scott Fister,