Qual è il modo più efficace per forzare il ridisegno di una UIView?


117

Ho un UITableView con un elenco di elementi. La selezione di un elemento spinge un viewController che quindi procede con le seguenti operazioni. dal metodo viewDidLoad lancio una URLRequest per i dati richiesti da una delle mie sottoviste: una sottoclasse UIView con drawRect sovrascritto. Quando i dati arrivano dal cloud, inizio a costruire la mia gerarchia di visualizzazione. la sottoclasse in questione riceve i dati e il suo metodo drawRect ora ha tutto ciò di cui ha bisogno per il rendering.

Ma.

Poiché non chiamo drawRect esplicitamente - Cocoa-Touch lo gestisce - non ho modo di informare Cocoa-Touch che voglio davvero che questa sottoclasse UIView esegua il rendering. Quando? Adesso andrebbe bene!

Ho provato [myView setNeedsDisplay]. Questo tipo funziona a volte. Molto chiazzato.

Sto lottando con questo per ore e ore. Qualcuno che per favore mi fornisca un approccio solido e garantito per forzare un riesame di UIView.

Ecco lo snippet di codice che alimenta i dati alla vista:

// 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];

Salute, Doug

Risposte:


193

Il modo garantito e solido per forzare un UIViewrendering è [myView setNeedsDisplay]. Se hai problemi con questo, probabilmente stai riscontrando uno di questi problemi:

  • Lo stai chiamando prima di avere effettivamente i dati o stai -drawRect:memorizzando qualcosa nella cache.

  • Ti aspetti che la vista venga disegnata nel momento in cui chiami questo metodo. Non c'è intenzionalmente alcun modo per chiedere "disegnare in questo momento proprio in questo secondo" utilizzando il sistema di trafilatura Cocoa. Ciò interromperebbe l'intero sistema di composizione della vista, le prestazioni dei rifiuti e probabilmente creerebbe tutti i tipi di artefatti. Ci sono solo modi per dire "questo deve essere disegnato nel prossimo ciclo di estrazione".

Se ciò di cui hai bisogno è "un po 'di logica, disegna, un po' di logica in più", allora devi mettere "un po 'di logica in più" in un metodo separato e invocarlo usando -performSelector:withObject:afterDelay:con un ritardo di 0. Ciò metterà "un po' di logica in più" dopo il ciclo di estrazione successivo. Vedi questa domanda per un esempio di quel tipo di codice e un caso in cui potrebbe essere necessario (anche se di solito è meglio cercare altre soluzioni, se possibile, poiché complica il codice).

Se non pensi che le cose stiano andando a gonfie vele, inserisci un breakpoint -drawRect:e guarda quando vieni chiamato. Se stai chiamando -setNeedsDisplay, ma -drawRect:non vieni chiamato nel prossimo ciclo di eventi, quindi approfondisci la gerarchia della visualizzazione e assicurati di non provare a superare in astuzia da qualche parte. L'eccessiva intelligenza è la prima causa di cattivi disegni nella mia esperienza. Quando pensi di sapere meglio come indurre il sistema a fare ciò che vuoi, di solito fai in modo che faccia esattamente ciò che non vuoi.


Rob, ecco la mia lista di controllo. 1) Ho i dati? Sì. Creo la vista in un metodo - hideSpinner - chiamato sul thread principale dal connectionDidFinishLoading: così: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Non ho bisogno di disegnare immediatamente. Ne ho solo bisogno. Oggi. Attualmente è completamente casuale e fuori dal mio controllo. [myView setNeedsDisplay] è completamente inaffidabile. Sono persino arrivato al punto di chiamare [myView setNeedsDisplay] in viewDidAppear :. Nuthin'. Il cacao semplicemente mi ignora. Esasperante !!
dugla

1
Ho scoperto che con questo approccio c'è spesso un ritardo tra la setNeedsDisplaychiamata e l'effettiva chiamata di drawRect:. Anche se l'affidabilità della chiamata è qui, non la definirei la soluzione "più robusta": una soluzione di disegno robusta dovrebbe, in teoria, fare tutto il disegno immediatamente prima di tornare al codice client che richiede il disegno.
Slipp D. Thompson

1
Ho commentato di seguito la tua risposta con maggiori dettagli. Cercare di forzare il sistema di disegno a funzionare fuori servizio chiamando metodi che la documentazione dice esplicitamente di non chiamare non è robusto. Nella migliore delle ipotesi è un comportamento indefinito. Molto probabilmente ridurrà le prestazioni e la qualità del disegno. Nel peggiore dei casi potrebbe bloccarsi o bloccarsi.
Rob Napier

1
Se devi disegnare prima di tornare, disegna in un contesto di immagine (che funziona proprio come la tela che molte persone si aspettano). Ma non cercare di ingannare il sistema di compositing. È progettato per fornire grafica di alta qualità molto velocemente con un sovraccarico minimo di risorse. Parte di ciò è l'unificazione dei passaggi di disegno alla fine del ciclo di esecuzione.
Rob Napier

1
@ RobNapier Non capisco perché stai diventando così difensivo. Si prega di ricontrollare; non vi è alcuna violazione dell'API (sebbene sia stata ripulita in base al tuo suggerimento, direi che chiamare il proprio metodo non è una violazione), né possibilità di deadlock o crash. Inoltre non è un "trucco"; utilizza setNeedsDisplay / displayIfNeeded di CALayer in modo normale. Inoltre, lo uso da un po 'di tempo per disegnare con Quartz in modo parallelo a GLKView / OpenGL; si è dimostrato sicuro, stabile e più veloce della soluzione che hai elencato. Se non mi credi, provalo. Non hai niente da perdere qui.
Slipp D. Thompson

52

Ho avuto un problema con un grande ritardo tra la chiamata di setNeedsDisplay e drawRect: (5 secondi). Si è scoperto che ho chiamato setNeedsDisplay in un thread diverso dal thread principale. Dopo aver spostato questa chiamata sul thread principale, il ritardo è andato via.

Spero che questo sia di qualche aiuto.


Questo era sicuramente il mio bug. Avevo l'impressione di essere in esecuzione sul thread principale, ma è stato solo quando ho eseguito NSLogged NSThread.isMainThread che mi sono reso conto che c'era un caso d'angolo in cui NON stavo apportando le modifiche ai livelli sul thread principale. Grazie per avermi salvato dallo strapparmi i capelli!
Mr. T

14

Il modo con rimborso garantito, cemento armato-solido per forzare una vista a disegnare in modo sincrono (prima di tornare al codice chiamante) è configurare le CALayerinterazioni di con la tua UIViewsottoclasse.

Nella tua sottoclasse UIView, crea un displayNow()metodo che dica al livello di " impostare la rotta per la visualizzazione ", quindi di " renderla tale ":

veloce

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

Objective-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];
}

Implementa anche un draw(_: CALayer, in: CGContext)metodo che chiamerà il tuo metodo di disegno privato / interno (che funziona poiché ogni UIViewè a CALayerDelegate) :

veloce

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

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

E crea il tuo internalDraw(_: CGRect)metodo personalizzato , insieme a fail-safe draw(_: CGRect):

veloce

/// 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)
}

Objective-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];
}

E ora chiama semplicemente myView.displayNow()ogni volta che ne hai davvero bisogno per disegnare (come da una CADisplayLinkrichiamata) . Il nostro displayNow()metodo dirà al CALayerto displayIfNeeded(), che richiamerà in modo sincrono nel nostro draw(_:,in:)e farà il disegno internalDraw(_:), aggiornando l'immagine con ciò che è disegnato nel contesto prima di andare avanti.


Questo approccio è simile a quello di @ RobNapier sopra, ma ha il vantaggio di chiamare displayIfNeeded()in aggiunta a setNeedsDisplay(), il che lo rende sincrono.

Ciò è possibile perché CALayers espone più funzionalità di disegno rispetto a UIViews: i livelli sono di livello inferiore rispetto alle viste e progettati esplicitamente allo scopo di disegnare altamente configurabili all'interno del layout e (come molte cose in Cocoa) sono progettati per essere utilizzati in modo flessibile ( come classe genitore, o come delegante, o come ponte verso altri sistemi di disegno, o semplicemente da soli). Il corretto utilizzo del CALayerDelegateprotocollo rende tutto questo possibile.

Ulteriori informazioni sulla configurabilità di CALayers sono disponibili nella sezione Configurazione di oggetti livello della Guida alla programmazione dell'animazione di base .


Si noti che la documentazione per drawRect:dice esplicitamente "Non dovresti mai chiamare questo metodo direttamente da solo". Inoltre, CALayer displaydice esplicitamente "Non chiamare direttamente questo metodo". Se si desidera disegnare in modo sincrono contentsdirettamente sui livelli, non è necessario violare queste regole. Puoi semplicemente disegnare sul livello contentsogni volta che vuoi (anche sui thread in background). Basta aggiungere un sottolivello alla vista per questo. Ma questo è diverso dal metterlo sullo schermo, che deve attendere fino al momento di composizione corretto.
Rob Napier

Dico di aggiungere un sottolivello poiché i documenti avvertono anche di interferire direttamente con il livello di UIView contents("Se l'oggetto del livello è legato a un oggetto vista, dovresti evitare di impostare direttamente il contenuto di questa proprietà. L'interazione tra viste e livelli di solito risulta nella vista che sostituisce il contenuto di questa proprietà durante un successivo aggiornamento. ") Non sto particolarmente raccomandando questo approccio; il disegno prematuro mina le prestazioni e la qualità del disegno. Ma se ne hai bisogno per qualche motivo, allora contentsè come ottenerlo.
Rob Napier

@ RobNapier Point preso con la chiamata drawRect:direttamente. Non era necessario dimostrare questa tecnica ed è stata risolta.
Slipp D. Thompson

@RobNapier Per quanto riguarda la contentstecnica che stai suggerendo ... sembra allettante. Ho provato qualcosa del genere inizialmente ma non sono riuscito a farlo funzionare e ho trovato la soluzione di cui sopra per essere molto meno codice senza motivo per non essere altrettanto performante. Tuttavia, se hai una soluzione funzionante per l' contentsapproccio, sarei interessato a leggerlo (non c'è motivo per cui non puoi avere due risposte a questa domanda, giusto?)
Slipp D. Thompson

@ RobNapier Nota: questo non ha nulla a che fare con CALayer display. È solo un metodo pubblico personalizzato in una sottoclasse UIView, proprio come ciò che viene fatto in GLKView (un'altra sottoclasse UIView e l'unica scritta da Apple che conosco che richiede la funzionalità DRAW NOW! ).
Slipp D. Thompson

5

Ho avuto lo stesso problema e tutte le soluzioni di SO o Google non hanno funzionato per me. Di solito, setNeedsDisplayfunziona, ma quando non funziona ...
Ho provato a chiamare setNeedsDisplayla vista in ogni modo possibile da ogni possibile thread e roba - ancora senza successo. Lo sappiamo, come ha detto Rob, questo

"questo deve essere disegnato nel prossimo ciclo di sorteggio."

Ma per qualche motivo questa volta non avrebbe disegnato. E l'unica soluzione che ho trovato è chiamarlo manualmente dopo un po 'di tempo, per lasciare passare tutto ciò che blocca il sorteggio, in questo modo:

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

È una buona soluzione se non è necessario che la vista venga ridisegnata molto spesso. Altrimenti, se stai facendo delle cose in movimento (azione), di solito non ci sono problemi con la semplice chiamatasetNeedsDisplay .

Spero che possa aiutare qualcuno che si è perso lì, come me.


0

Beh, so che questo potrebbe essere un grande cambiamento o addirittura non adatto al tuo progetto, ma hai considerato di non eseguire il push finché non hai già i dati ? In questo modo è sufficiente disegnare la vista una sola volta e anche l'esperienza dell'utente sarà migliore: la spinta si sposterà già caricata.

Il modo in cui lo fai è nel UITableView didSelectRowAtIndexPathchiedere in modo asincrono i dati. Dopo aver ricevuto la risposta, esegui manualmente il segue e passi i dati al tuo viewController in prepareForSegue. Nel frattempo potresti voler mostrare qualche indicatore di attività, per un semplice indicatore di caricamento controlla https://github.com/jdg/MBProgressHUD

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.