¿Cuál es la forma más sólida de forzar a que una UIView se vuelva a dibujar?

117

Tengo un UITableView con una lista de elementos. Seleccionar un elemento empuja un viewController que luego procede a hacer lo siguiente. desde el método viewDidLoad, disparo una URLRequest para los datos que requiere una de mis subvistas: una subclase UIView con drawRect anulado. Cuando los datos llegan de la nube, empiezo a construir mi jerarquía de vistas. la subclase en cuestión recibe los datos y su método drawRect ahora tiene todo lo que necesita para renderizar.

Pero.

Debido a que no llamo a drawRect explícitamente, Cocoa-Touch se encarga de eso, no tengo forma de informar a Cocoa-Touch que realmente quiero que se procese esta subclase de UIView. ¿Cuando? ¡Ahora estaría bien!

He probado [myView setNeedsDisplay]. Esto funciona a veces. Muy irregular.

He estado luchando con esto durante horas y horas. ¿Podría alguien que me brinde un enfoque sólido y garantizado para forzar una re-renderización de UIView?

Este es el fragmento de código que envía datos a la 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];

Saludos, Doug

dugla
fuente

Respuestas:

193

La forma garantizada y sólida como una roca de forzar a un UIViewrenderizado es [myView setNeedsDisplay]. Si tiene problemas con eso, es probable que se encuentre con uno de estos problemas:

  • Lo está llamando antes de tener los datos, o -drawRect:está sobrecargando algo.

  • Espera que la vista se dibuje en el momento en que llama a este método. No hay forma intencional de exigir "dibujar ahora mismo en este mismo segundo" utilizando el sistema de dibujo Cocoa. Eso interrumpiría todo el sistema de composición de vistas, arruinaría el rendimiento y probablemente crearía todo tipo de artefactos. Solo hay formas de decir "esto debe dibujarse en el próximo ciclo de sorteo".

Si lo que necesita es "algo de lógica, dibujo, algo más de lógica", entonces debe poner "algo más de lógica" en un método separado e invocarlo -performSelector:withObject:afterDelay:con un retraso de 0. Eso pondrá "algo más de lógica" después el próximo ciclo de sorteo. Consulte esta pregunta para ver un ejemplo de ese tipo de código y un caso en el que podría ser necesario (aunque generalmente es mejor buscar otras soluciones si es posible, ya que complica el código).

Si cree que las cosas no se están dibujando, coloque un punto de interrupción -drawRect:y vea cuándo lo llaman. Si está llamando -setNeedsDisplay, pero -drawRect:no recibe la llamada en el siguiente ciclo de eventos, busque en la jerarquía de su vista y asegúrese de que no está tratando de ser más astuto que en alguna parte. En mi experiencia, el exceso de inteligencia es la causa número uno de los malos dibujos. Cuando cree que sabe mejor cómo engañar al sistema para que haga lo que quiere, normalmente consigue que haga exactamente lo que no quiere.

Rob Napier
fuente
Rob, aquí está mi lista de verificación. 1) ¿Tengo los datos? Si. Creo la vista en un método - hideSpinner - llamado en el hilo principal de connectionDidFinishLoading: así: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) No necesito dibujar al instante. Solo lo necesito. Hoy. Actualmente, es completamente aleatorio y está fuera de mi control. [myView setNeedsDisplay] es completamente poco confiable. Incluso he ido tan lejos como para llamar a [myView setNeedsDisplay] en viewDidAppear :. Nada. Cocoa simplemente me ignora. ¡¡Enloquecedor!!
dugla
1
Descubrí que con este enfoque a menudo hay un retraso entre la setNeedsDisplayllamada real y la de drawRect:. Si bien la confiabilidad de la llamada está aquí, yo no llamaría a esta la solución “más robusta”; una solución de dibujo robusta debería, en teoría, hacer todo el dibujo inmediatamente antes de regresar al código del cliente que exige el dibujo.
Slipp D. Thompson
1
Comenté a continuación su respuesta con más detalles. Tratar de forzar al sistema de dibujo a funcionar fuera de servicio llamando a métodos que la documentación dice explícitamente no es robusto. En el mejor de los casos, es un comportamiento indefinido. Lo más probable es que disminuya el rendimiento y la calidad del dibujo. En el peor de los casos, podría bloquearse o bloquearse.
Rob Napier
1
Si necesita dibujar antes de regresar, dibuje en un contexto de imagen (que funciona como el lienzo que muchas personas esperan). Pero no intente engañar al sistema de composición. Está diseñado para proporcionar gráficos de alta calidad muy rápido con una sobrecarga de recursos mínima. Parte de eso es unir pasos de dibujo al final del ciclo de ejecución.
Rob Napier
1
@RobNapier No entiendo por qué te estás poniendo tan a la defensiva. Por favor revise de nuevo; no hay una infracción de API (aunque se ha limpiado según su sugerencia, yo diría que llamar al método propio no es una infracción), ni posibilidad de bloqueo o bloqueo. Tampoco es un "truco"; utiliza setNeedsDisplay / displayIfNeeded de CALayer de forma normal. Además, lo he estado usando durante bastante tiempo para dibujar con Quartz de manera paralela a GLKView / OpenGL; ha demostrado ser seguro, estable y más rápido que la solución que ha enumerado. Si no me cree, inténtelo. No tienes nada que perder aquí.
Slipp D. Thompson
52

Tuve un problema con un gran retraso entre llamar a setNeedsDisplay y drawRect: (5 segundos). Resultó que llamé setNeedsDisplay en un hilo diferente al hilo principal. Después de mover esta llamada al hilo principal, el retraso desapareció.

Espero que esto sea de alguna ayuda.

Werner Altewischer
fuente
Este fue definitivamente mi error. Tenía la impresión de que estaba ejecutando en el hilo principal, pero no fue hasta que NSLogged NSThread.isMainThread que me di cuenta de que había un caso de esquina en el que NO estaba haciendo los cambios de capa en el hilo principal. ¡Gracias por salvarme de arrancarme el pelo!
Sr. T
14

La forma de devolución de dinero garantizada, de hormigón armado y sólido de forzar a que una vista se dibuje sincrónicamente (antes de volver al código de llamada) es configurar las CALayerinteracciones de the con su UIViewsubclase.

En su subclase UIView, cree un displayNow()método que le diga a la capa que " establezca el rumbo para la visualización " y luego "lo haga así ":

Rápido

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

C objetivo

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

También implemente un draw(_: CALayer, in: CGContext)método que llamará a su método de dibujo privado / interno (que funciona ya que cada UIViewes a CALayerDelegate) :

Rápido

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

C objetivo

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

Y cree su internalDraw(_: CGRect)método personalizado , junto con a prueba de fallas draw(_: CGRect):

Rápido

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

C objetivo

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

Y ahora solo llame myView.displayNow()cuando realmente lo necesite para dibujar (como desde una CADisplayLinkdevolución de llamada) . Nuestro displayNow()método le dirá CALayera displayIfNeeded(), que llamará de forma sincrónica a nuestro draw(_:,in:)y hará el dibujo internalDraw(_:), actualizando lo visual con lo que se dibuja en el contexto antes de continuar.


Este enfoque es similar al de @ RobNapier anterior, pero tiene la ventaja de llamar displayIfNeeded()además de setNeedsDisplay(), lo que lo hace sincrónico.

Esto es posible porque los CALayers exponen más funciones de dibujo que los UIViews: las capas tienen un nivel más bajo que las vistas y están diseñadas explícitamente con el propósito de un dibujo altamente configurable dentro del diseño y (como muchas cosas en Cocoa) están diseñadas para usarse de manera flexible ( como clase principal, o como delegador, o como puente a otros sistemas de dibujo, o simplemente por sí mismos). El uso adecuado del CALayerDelegateprotocolo hace que todo esto sea posible.

CALayerPuede encontrar más información sobre la capacidad de configuración de s en la sección Configuración de objetos de capa de la Guía de programación de animación principal .

Slipp D. Thompson
fuente
Tenga en cuenta que la documentación de drawRect:dice explícitamente "Nunca debe llamar a este método directamente usted mismo". Además, CALayer displaydice explícitamente "No llame a este método directamente". Si desea dibujar contentsdirectamente en las capas sincrónicamente, no es necesario violar estas reglas. Puede dibujar en la capa en contentscualquier momento que desee (incluso en hilos de fondo). Simplemente agregue una subcapa a la vista para eso. Pero eso es diferente a ponerlo en la pantalla, que necesita esperar hasta el momento de composición correcto.
Rob Napier
Digo agregar una subcapa ya que los documentos también advierten sobre jugar directamente con la capa de UIView contents("Si el objeto de la capa está vinculado a un objeto de vista, debe evitar establecer el contenido de esta propiedad directamente. La interacción entre las vistas y las capas generalmente resulta en la vista que reemplaza el contenido de esta propiedad durante una actualización posterior. ") No estoy recomendando particularmente este enfoque; el dibujo prematuro socava el rendimiento y la calidad del dibujo. Pero si lo necesita por alguna razón, entonces contentses cómo conseguirlo.
Rob Napier
@RobNapier Point tomado con llamar drawRect:directamente. No era necesario demostrar esta técnica y se ha corregido.
Slipp D. Thompson
@RobNapier En cuanto a la contentstécnica que estás sugiriendo ... suena tentadora. Intenté algo así inicialmente, pero no pude hacer que funcionara, y descubrí que la solución anterior era mucho menos código sin ninguna razón para no tener el mismo rendimiento. Sin embargo, si tiene una solución funcional para el contentsenfoque, me interesaría leerla (no hay ninguna razón por la que no pueda tener dos respuestas a esta pregunta, ¿verdad?)
Slipp D. Thompson
@RobNapier Nota: Esto no tiene nada que ver con CALayer's display. Es solo un método público personalizado en una subclase de UIView, al igual que lo que se hace en GLKView (otra subclase de UIView, y el único escrito por Apple que conozco que exige la funcionalidad DRAW NOW! ).
Slipp D. Thompson
5

Tuve el mismo problema y todas las soluciones de SO o Google no funcionaron para mí. Por lo general, setNeedsDisplayfunciona, pero cuando no ...
Intenté llamar setNeedsDisplaya la vista de todas las formas posibles desde todos los hilos posibles y todo eso, pero todavía no tuve éxito. Sabemos, como dijo Rob, que

"esto debe dibujarse en el próximo ciclo de sorteo".

Pero por alguna razón no dibujaría esta vez. Y la única solución que he encontrado es llamarlo manualmente después de un tiempo, para dejar que todo lo que bloquee el sorteo pase, así:

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

Es una buena solución si no necesita que la vista vuelva a dibujar con mucha frecuencia. De lo contrario, si está haciendo algo en movimiento (acción), generalmente no hay problemas con solo llamar setNeedsDisplay.

Espero que ayude a alguien que está perdido allí, como yo.

Dreamzor
fuente
0

Bueno, sé que esto podría ser un gran cambio o incluso no ser adecuado para su proyecto, pero ¿consideró no realizar el empuje hasta que ya tenga los datos ? De esa manera, solo necesita dibujar la vista una vez y la experiencia del usuario también será mejor: el empuje se moverá ya cargado.

La forma en que lo hace es en la UITableView didSelectRowAtIndexPathque solicita los datos de forma asincrónica. Una vez que recibe la respuesta, realiza manualmente la transición y pasa los datos a su viewController en formato prepareForSegue. Mientras tanto, es posible que desee mostrar algún indicador de actividad, para verificar el indicador de carga simple https://github.com/jdg/MBProgressHUD

Miki
fuente