Jaki jest najpewniejszy sposób wymuszenia przerysowania UIView?

117

Mam UITableView z listą elementów. Wybranie elementu powoduje wypchnięcie elementu viewController, który następnie wykonuje następujące czynności. z metody viewDidLoad Odpalam żądanie URLRequest dla danych, które są wymagane przez jedną z moich podglądów podrzędnych - podklasę UIView z nadpisaniem funkcji drawRect. Gdy dane docierają z chmury, zaczynam budować swoją hierarchię widoków. dana podklasa otrzymuje dane, a jej metoda drawRect ma teraz wszystko, czego potrzebuje do renderowania.

Ale.

Ponieważ nie wywołuję drawRect jawnie - Cocoa-Touch to obsługuje - nie mam możliwości poinformowania Cocoa-Touch, że naprawdę chcę, aby ta podklasa UIView była renderowana. Kiedy? Teraz byłoby dobrze!

Próbowałem [myView setNeedsDisplay]. To czasami działa. Bardzo nierówny.

Zmagam się z tym przez wiele godzin. Czy może ktoś, kto zapewni mi solidne, gwarantowane podejście do wymuszania ponownego renderowania UIView.

Oto fragment kodu, który dostarcza dane do widoku:

// Create the subview
self.chromosomeBlockView = [[[ChromosomeBlockView alloc] initWithFrame:frame] autorelease];

// Set some properties
self.chromosomeBlockView.sequenceString     = self.sequenceString;
self.chromosomeBlockView.nucleotideBases    = self.nucleotideLettersDictionary;

// Insert the view in the view hierarchy
[self.containerView          addSubview:self.chromosomeBlockView];
[self.containerView bringSubviewToFront:self.chromosomeBlockView];

// A vain attempt to convince Cocoa-Touch that this view is worthy of being displayed ;-)
[self.chromosomeBlockView setNeedsDisplay];

Pozdrawiam, Doug

dugla
źródło

Odpowiedzi:

193

Gwarantowanym, solidnym sposobem na wymuszenie UIViewponownego renderowania jest [myView setNeedsDisplay]. Jeśli masz z tym problem, prawdopodobnie napotkasz jeden z następujących problemów:

  • Wywołujesz to, zanim faktycznie masz dane, lub -drawRect:nadmiernie buforujesz coś.

  • Oczekujesz, że widok zostanie narysowany w momencie wywołania tej metody. Celowo nie ma sposobu, aby żądać „narysuj teraz w tej sekundzie” za pomocą systemu rysowania Cocoa. To zakłóciłoby cały system komponowania widoku, zepsuło działanie i prawdopodobnie spowodowałoby wszelkiego rodzaju artefakty. Istnieją tylko sposoby, aby powiedzieć, że „to musi zostać narysowane w następnym cyklu losowania”.

Jeśli potrzebujesz „trochę logiki, rysuj, trochę więcej logiki”, to musisz umieścić „trochę więcej logiki” w oddzielnej metodzie i wywołać ją -performSelector:withObject:afterDelay:z opóźnieniem równym 0. To spowoduje wstawienie „trochę więcej logiki” po następny cykl losowania. Zobacz to pytanie, aby zobaczyć przykład tego rodzaju kodu i przypadek, w którym może być potrzebny (chociaż zwykle najlepiej jest szukać innych rozwiązań, jeśli to możliwe, ponieważ komplikuje kod).

Jeśli nie sądzisz, że coś się rysuje, ustaw punkt przerwania -drawRect:i zobacz, kiedy zostaniesz wezwany. Jeśli dzwonisz -setNeedsDisplay, ale -drawRect:nie otrzymujesz wezwania w następnej pętli zdarzeń, zagłęb się w hierarchię widoku i upewnij się, że nie próbujesz przechytrzyć, gdzieś. Z mojego doświadczenia wynika, że ​​nadmierna inteligencja jest główną przyczyną złego rysowania. Kiedy myślisz, że najlepiej wiesz, jak oszukać system, aby robił to, co chcesz, zwykle robisz dokładnie to, czego nie chcesz.

Rob Napier
źródło
Rob, oto moja lista kontrolna. 1) Czy mam dane? Tak. Widok tworzę w metodzie - hideSpinner - wywołanej w głównym wątku z connectionDidFinishLoading: w ten sposób: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Nie potrzebuję rysować od razu. Po prostu tego potrzebuję. Dzisiaj. Obecnie jest to całkowicie losowe i poza moją kontrolą. [myView setNeedsDisplay] jest całkowicie zawodne. Posunąłem się nawet do wywołania [myView setNeedsDisplay] w viewDidAppear :. Nic. Cocoa po prostu mnie ignoruje. Szalony!!
dugla
1
Zauważyłem, że przy takim podejściu często występuje opóźnienie między setNeedsDisplaywywołaniem a rzeczywistym wywołaniem drawRect:. Chociaż istnieje tutaj niezawodność wywołania, nie nazwałbym tego „najbardziej niezawodnym” rozwiązaniem - solidne rozwiązanie do rysowania powinno teoretycznie wykonywać cały rysunek bezpośrednio przed powrotem do kodu klienta, który żąda rysowania.
Slipp D. Thompson
1
Poniżej skomentowałem Twoją odpowiedź, podając więcej szczegółów. Próba wymuszenia na systemie rysowania, aby przestał działać, przez wywołanie metod, które dokumentacja wyraźnie mówi, aby nie wywoływać, nie jest skuteczna. W najlepszym przypadku jest to niezdefiniowane zachowanie. Najprawdopodobniej pogorszy to wydajność i jakość rysunku. W najgorszym przypadku może dojść do impasu lub awarii.
Rob Napier
1
Jeśli musisz narysować przed powrotem, narysuj kontekst obrazu (który działa tak, jak płótno, którego oczekuje wiele osób). Ale nie próbuj oszukiwać systemu kompozycji. Został zaprojektowany w celu bardzo szybkiego zapewnienia wysokiej jakości grafiki przy minimalnym obciążeniu zasobów. Częścią tego jest łączenie kroków rysowania na końcu pętli uruchamiania.
Rob Napier
1
@RobNapier Nie rozumiem, dlaczego stajesz się taki defensywny. Prosze Sprawdź ponownie; nie ma naruszenia API (chociaż zostało oczyszczone w oparciu o twoją sugestię, argumentowałbym, że wywołanie własnej metody nie jest naruszeniem) ani szansy na zakleszczenie lub awarię. To też nie jest „sztuczka”; używa CALayer setNeedsDisplay / displayIfNeeded w normalny sposób. Co więcej, używam go już od dłuższego czasu, aby rysować za pomocą Quartz w sposób równoległy do ​​GLKView / OpenGL; okazało się, że jest bezpieczny, stabilny i szybszy niż wymienione przez Ciebie rozwiązanie. Jeśli mi nie wierzysz, spróbuj. Nie masz tu nic do stracenia.
Slipp D. Thompson
52

Miałem problem z dużym opóźnieniem między wywołaniami setNeedsDisplay i drawRect: (5 sekund). Okazało się, że nazwałem setNeedsDisplay w innym wątku niż główny. Po przeniesieniu tego wywołania do głównego wątku opóźnienie zniknęło.

Mam nadzieję, że to pomoże.

Werner Altewischer
źródło
To był zdecydowanie mój błąd. Miałem wrażenie, że biegam na głównym wątku, ale dopiero gdy NSLogged NSThread.isMainThread zdałem sobie sprawę, że był przypadek narożny, w którym NIE dokonywałem zmian warstwy w głównym wątku. Dzięki za uratowanie mnie przed wyrywaniem włosów!
Mr. T
14

Gwarantowany zwrot pieniędzy, solidny sposób na wymuszenie synchronicznego rysowania widoku (przed powrotem do kodu wywołującego) polega na skonfigurowaniu CALayerinterakcji między UIViewpodklasą.

W swojej podklasie UIView utwórz displayNow()metodę, która powie warstwie, aby „ ustawiła kurs wyświetlania ”, a następnie „ uczyniła to ”:

Szybki

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
public func displayNow()
{
    let layer = self.layer
    layer.setNeedsDisplay()
    layer.displayIfNeeded()
}

Cel C

/// Redraws the view's contents immediately.
/// Serves the same purpose as the display method in GLKView.
- (void)displayNow
{
    CALayer *layer = self.layer;
    [layer setNeedsDisplay];
    [layer displayIfNeeded];
}

Zaimplementuj także draw(_: CALayer, in: CGContext)metodę, która wywoła twoją prywatną / wewnętrzną metodę rysowania (która działa, ponieważ każdy UIViewjest a CALayerDelegate) :

Szybki

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
override func draw(_ layer: CALayer, in context: CGContext)
{
    UIGraphicsPushContext(context)
    internalDraw(self.bounds)
    UIGraphicsPopContext()
}

Cel C

/// Called by our CALayer when it wants us to draw
///     (in compliance with the CALayerDelegate protocol).
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
    UIGraphicsPushContext(context);
    [self internalDrawWithRect:self.bounds];
    UIGraphicsPopContext();
}

I stwórz własną internalDraw(_: CGRect)metodę wraz z niezawodnością draw(_: CGRect):

Szybki

/// Internal drawing method; naming's up to you.
func internalDraw(_ rect: CGRect)
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
override func draw(_ rect: CGRect) {
    internalDraw(rect)
}

Cel C

/// Internal drawing method; naming's up to you.
- (void)internalDrawWithRect:(CGRect)rect
{
    // @FILLIN: Custom drawing code goes here.
    //  (Use `UIGraphicsGetCurrentContext()` where necessary.)
}

/// For compatibility, if something besides our display method asks for draw.
- (void)drawRect:(CGRect)rect {
    [self internalDrawWithRect:rect];
}

A teraz po prostu zadzwoń, myView.displayNow()kiedy naprawdę - naprawdę potrzebujesz go do rysowania (na przykład z CADisplayLinkoddzwonienia) . Nasza displayNow()metoda powie CALayerto displayIfNeeded(), co będzie synchronicznie wywoływać z powrotem do naszego draw(_:,in:)i rysować internalDraw(_:), aktualizując wizualizację o to, co zostało wciągnięte w kontekst, zanim przejdziemy dalej.


To podejście jest podobne do powyższego @ RobNapier, ale ma tę zaletę, że displayIfNeeded()oprócz wywoływania setNeedsDisplay()jest synchroniczne.

Jest to możliwe, ponieważ CALayerudostępniają one więcej funkcji rysowania niż inne - UIViewwarstwy są niższego poziomu niż widoki i zaprojektowane specjalnie w celu tworzenia wysoce konfigurowalnych rysunków w układzie i (podobnie jak wiele rzeczy w kakao) są zaprojektowane tak, aby można było ich używać elastycznie ( jako klasa nadrzędna lub delegator, lub jako pomost do innych systemów rysowania lub po prostu samodzielnie). To wszystko umożliwia właściwe wykorzystanie CALayerDelegateprotokołu.

Więcej informacji na temat konfigurowalności CALayers można znaleźć w sekcji Setting Up Layer Objects w przewodniku Core Animation Programming Guide .

Slipp D. Thompson
źródło
Zauważ, że dokumentacja drawRect:wyraźnie mówi: „Nigdy nie powinieneś bezpośrednio wywoływać tej metody”. Ponadto CALayer displaywyraźnie mówi „Nie wywołuj tej metody bezpośrednio”. Jeśli chcesz synchronicznie rysować contentsbezpośrednio na warstwach, nie ma potrzeby naruszania tych zasad. Możesz po prostu rysować na warstwie w contentsdowolnym momencie (nawet w wątkach w tle). W tym celu wystarczy do widoku dodać podwarstwę. Ale to coś innego niż umieszczenie go na ekranie, który musi poczekać do właściwego czasu komponowania.
Rob Napier
Mówię, aby dodać podwarstwę, ponieważ dokumenty ostrzegają również o bezpośrednim manipulowaniu warstwą UIView contents(„Jeśli obiekt warstwy jest powiązany z obiektem widoku, należy unikać bezpośredniego ustawiania zawartości tej właściwości. Interakcja między widokami i warstwami zwykle skutkuje w celu zastąpienia zawartości tej właściwości podczas kolejnej aktualizacji. ”) Nie polecam szczególnie tego podejścia; przedwczesne rysowanie obniża wydajność i jakość rysunku. Ale jeśli potrzebujesz tego z jakiegoś powodu, contentsto jak to zdobyć.
Rob Napier
@RobNapier Punkt wzięty z dzwonieniem drawRect:bezpośrednio. Nie było potrzeby demonstrowania tej techniki i zostało naprawione.
Slipp D. Thompson
@RobNapier Jeśli chodzi o contentstechnikę, którą proponujesz… brzmi zachęcająco. Próbowałem czegoś takiego na początku, ale nie mogłem go uruchomić i stwierdziłem, że powyższe rozwiązanie zawiera znacznie mniej kodu bez powodu, aby nie być tak wydajnym. Jeśli jednak masz działające rozwiązanie dla tego contentspodejścia, byłbym zainteresowany jego przeczytaniem (nie ma powodu, dla którego nie możesz mieć dwóch odpowiedzi na to pytanie, prawda?)
Slipp D. Thompson
@RobNapier Uwaga: to nie ma nic wspólnego z CALayer's display. Jest to po prostu niestandardowa metoda publiczna w podklasie UIView, tak jak to się robi w GLKView (kolejna podklasa UIView i jedyna napisana przez Apple, o której wiem, a która wymaga funkcji DRAW NOW! ).
Slipp D. Thompson
5

Miałem ten sam problem, a wszystkie rozwiązania z SO czy Google nie działały dla mnie. Zwykle setNeedsDisplaydziała, ale jeśli nie ...
Próbowałem wywoływać setNeedsDisplaywidok w każdy możliwy sposób ze wszystkich możliwych wątków i innych rzeczy - nadal bez sukcesu. Wiemy, jak powiedział Rob, że

„należy to narysować w następnym cyklu losowania”.

Ale z jakiegoś powodu tym razem by nie rysował. Jedynym rozwiązaniem, które znalazłem, jest wywołanie go ręcznie po pewnym czasie, aby wszystko, co blokuje losowanie, przeminęło, na przykład:

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 
                                        (int64_t)(0.005 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
    [viewToRefresh setNeedsDisplay];
});

To dobre rozwiązanie, jeśli widok nie jest potrzebny do częstego przerysowywania. W przeciwnym razie, jeśli robisz jakieś ruchy (akcje), zwykle nie ma problemów z samym sprawdzeniem setNeedsDisplay.

Mam nadzieję, że pomoże to komuś, kto się tam zgubił, tak jak ja.

dreamzor
źródło
0

Cóż, wiem, że może to być duża zmiana lub nawet nieodpowiednia dla twojego projektu, ale czy rozważałeś niewykonanie wypychania, dopóki nie masz już danych ? W ten sposób wystarczy tylko raz narysować widok, a wrażenia użytkownika również będą lepsze - przycisk zostanie już załadowany.

Sposób, w jaki to robisz, polega na UITableView didSelectRowAtIndexPathasynchronicznym pytaniu o dane. Po otrzymaniu odpowiedzi należy ręcznie wykonać płynność i przekazać dane do elementu viewController w prepareForSegue. W międzyczasie możesz chcieć wyświetlić wskaźnik aktywności, aby uzyskać prosty wskaźnik ładowania, sprawdź https://github.com/jdg/MBProgressHUD

Miki
źródło