Qual é a maneira mais robusta de forçar um UIView a redesenhar?

117

Eu tenho um UITableView com uma lista de itens. Selecionar um item empurra um viewController que então prossegue para fazer o seguinte. do método viewDidLoad eu disparo um URLRequest para dados que são exigidos por uma das minhas subvisualizações - uma subclasse UIView com drawRect substituído. Quando os dados chegam da nuvem, começo a construir minha hierarquia de visualizações. a subclasse em questão passa os dados e seu método drawRect agora tem tudo o que precisa para renderizar.

Mas.

Porque eu não chamo drawRect explicitamente - Cocoa-Touch lida com isso - não tenho como informar ao Cocoa-Touch que eu realmente quero que esta subclasse UIView renderize. Quando? Agora seria bom!

Eu tentei [myView setNeedsDisplay]. Isso meio que funciona às vezes. Muito irregular.

Estou lutando com isso por horas e horas. Alguém poderia me fornecer uma abordagem sólida e garantida para forçar uma re-renderização do UIView.

Aqui está o snippet de código que alimenta os dados da visualização:

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

Saúde, Doug

dugla
fonte

Respostas:

193

A maneira garantida e sólida de forçar uma UIViewnova renderização é [myView setNeedsDisplay]. Se você está tendo problemas com isso, provavelmente está tendo um destes problemas:

  • Você está chamando antes de realmente ter os dados ou -drawRect:está armazenando algo em excesso no cache.

  • Você está esperando que a visualização seja desenhada no momento em que chamar esse método. Intencionalmente, não há como exigir "desenhar agora neste exato segundo" usando o sistema de desenho Cocoa. Isso interromperia todo o sistema de composição de visualização, prejudicaria o desempenho e provavelmente criaria todos os tipos de artefatos. Só existem maneiras de dizer "isso precisa ser desenhado no próximo ciclo de desenho".

Se você precisa de "um pouco de lógica, desenhe, um pouco mais de lógica", então você precisa colocar "um pouco mais de lógica" em um método separado e invocá-lo usando -performSelector:withObject:afterDelay:com um atraso de 0. Isso colocará "um pouco mais de lógica" depois o próximo ciclo de sorteio. Consulte esta pergunta para obter um exemplo desse tipo de código e um caso em que ele pode ser necessário (embora geralmente seja melhor procurar outras soluções, se possível, pois isso complica o código).

Se você acha que as coisas não estão sendo desenhadas, coloque um ponto de interrupção -drawRect:e veja quando você será chamado. Se você estiver chamando -setNeedsDisplay, mas -drawRect:não for chamado no próximo loop de evento, explore sua hierarquia de visualizações e certifique-se de que não está tentando ser mais esperto que está em algum lugar. O excesso de esperteza é a causa número 1 de um desenho ruim na minha experiência. Quando você acha que sabe como enganar o sistema para fazer o que você quer, geralmente consegue que ele faça exatamente o que não quer.

Rob Napier
fonte
Rob, aqui está minha lista de verificação. 1) Eu tenho os dados? Sim. Eu crio a visão em um método - hideSpinner - chamado no thread principal do connectionDidFinishLoading: assim: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Não preciso desenhar instantaneamente. Eu só preciso disso. Hoje. Atualmente, é completamente aleatório e fora do meu controle. [myView setNeedsDisplay] não é totalmente confiável. Cheguei até a chamar [myView setNeedsDisplay] em viewDidAppear :. Nuthin '. Cocoa simplesmente me ignora. Enlouquecedor !!
dugla
1
Descobri que, com essa abordagem, geralmente há um atraso entre a setNeedsDisplaychamada real de drawRect:. Embora a confiabilidade da chamada esteja aqui, eu não chamaria isso de solução “mais robusta” - uma solução de desenho robusta deve, em teoria, fazer todo o desenho imediatamente antes de retornar ao código do cliente que está exigindo o desenho.
Slipp D. Thompson
1
Comentei abaixo a sua resposta com mais detalhes. Tentar forçar o sistema de desenho a funcionar fora de ordem chamando métodos que a documentação diz explicitamente para não chamar não é robusto. Na melhor das hipóteses, é um comportamento indefinido. Muito provavelmente, isso prejudicará o desempenho e a qualidade do desenho. Na pior das hipóteses, pode travar ou travar.
Rob Napier
1
Se você precisar desenhar antes de retornar, desenhe em um contexto de imagem (que funciona exatamente como a tela que muitas pessoas esperam). Mas não tente enganar o sistema de composição. Ele foi projetado para fornecer gráficos de alta qualidade muito rapidamente com um mínimo de sobrecarga de recursos. Parte disso é aglutinar as etapas de desenho no final do loop de execução.
Rob Napier
1
@RobNapier Não entendo por que você está ficando tão defensivo. Por favor cheque novamente; não há violação de API (embora ela tenha sido limpa com base em sua sugestão, eu diria que chamar o próprio método não é uma violação), nem chance de deadlock ou travamento. Também não é um “truque”; ele usa setNeedsDisplay / displayIfNeeded do CALayer de uma maneira normal. Além disso, estou usando-o há um bom tempo para desenhar com Quartz de maneira paralela ao GLKView / OpenGL; provou ser seguro, estável e mais rápido do que a solução que você listou. Se você não acredita em mim, tente. Você não tem nada a perder aqui.
Slipp D. Thompson
52

Eu tive um problema com um grande atraso entre chamar setNeedsDisplay e drawRect: (5 segundos). Acontece que eu chamei setNeedsDisplay em um thread diferente do thread principal. Depois de mover esta chamada para o thread principal, o atraso foi embora.

Espero que isso ajude.

Werner Altewischer
fonte
Esse era definitivamente meu bug. Eu estava com a impressão de que estava executando no thread principal, mas não foi até que eu NSLogged NSThread.isMainThread que percebi que havia um caso em que eu NÃO estava fazendo as alterações de camada no thread principal. Obrigado por me salvar de puxar meu cabelo!
Sr. T
14

A maneira garantida de devolução do dinheiro e sólido de concreto armado de forçar uma visualização a desenhar de forma síncrona (antes de retornar ao código de chamada) é configurar as CALayerinterações de com sua UIViewsubclasse.

Em sua subclasse UIView, crie um displayNow()método que diga à camada para " definir o curso para exibição " e " torná-lo assim ":

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

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

Implemente também um draw(_: CALayer, in: CGContext)método que chamará seu método de desenho privado / interno (que funciona já que todo UIViewé um 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()
}

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 crie seu internalDraw(_: CGRect)método personalizado , junto com a prova de falhas 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)
}

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 agora apenas chame myView.displayNow()sempre que você realmente precisar para desenhar (como em um CADisplayLinkretorno de chamada) . Nosso displayNow()método dirá o CALayerpara displayIfNeeded(), que irá chamar de volta sincronizadamente o nosso draw(_:,in:)e desenhar internalDraw(_:), atualizando o visual com o que é desenhado no contexto antes de prosseguir.


Essa abordagem é semelhante à de @RobNapier acima, mas tem a vantagem de chamar displayIfNeeded()além de setNeedsDisplay(), o que a torna síncrona.

Isso é possível porque CALayers expõe mais funcionalidade de desenho do que UIViews - as camadas são de nível inferior do que as vistas e são projetadas explicitamente para o propósito de desenho altamente configurável dentro do layout e (como muitas coisas no Cocoa) são projetadas para serem usadas de forma flexível ( como uma classe pai, ou como um delegador, ou como uma ponte para outros sistemas de desenho, ou apenas por conta própria). O uso adequado do CALayerDelegateprotocolo torna tudo isso possível.

Mais informações sobre a configurabilidade de CALayers podem ser encontradas na seção Setting Up Layer Objects do Core Animation Programming Guide .

Slipp D. Thompson
fonte
Observe que a documentação para drawRect:diz explicitamente "Você nunca deve chamar este método diretamente." Além disso, o CALayer displaydiz explicitamente "Não chame este método diretamente." Se você deseja desenhar contentsdiretamente nas camadas, não há necessidade de violar essas regras. Você pode simplesmente desenhar na camada contentssempre que quiser (mesmo em threads de fundo). Basta adicionar uma subcamada à visualização para isso. Mas isso é diferente de colocá-lo na tela, que precisa esperar até o momento correto de composição.
Rob Napier
Eu digo para adicionar uma subcamada, pois os documentos também avisam sobre mexer diretamente com uma camada de UIView contents("Se o objeto de camada estiver vinculado a um objeto de visualização, você deve evitar definir o conteúdo desta propriedade diretamente. A interação entre visualizações e camadas geralmente resulta na visualização, substituindo o conteúdo dessa propriedade durante uma atualização subsequente. ") Não estou recomendando especificamente essa abordagem; o desenho prematuro prejudica o desempenho e a qualidade do desenho. Mas se você precisar dele por algum motivo, veja contentscomo obtê-lo.
Rob Napier
@RobNapier Ponto obtido com a chamada drawRect:direta. Não era necessário demonstrar esta técnica e foi corrigida.
Slipp D. Thompson
@RobNapier Quanto à contentstécnica que você está sugerindo ... parece atraente. Eu tentei algo parecido inicialmente, mas não consegui fazer funcionar e descobri que a solução acima era muito menos código, sem nenhuma razão para não ter o mesmo desempenho. No entanto, se você tiver uma solução de trabalho para a contentsabordagem, eu estaria interessado em lê-la (não há razão para que você não possa ter duas respostas para essa pergunta, certo?)
Slipp D. Thompson
@RobNapier Nota: Isso não tem nada a ver com CALayer display. É apenas um método público personalizado em uma subclasse UIView, assim como o que é feito em GLKView (outra subclasse UIView, e a única escrita pela Apple que conheço que exige a funcionalidade DRAW NOW! ).
Slipp D. Thompson
5

Eu tive o mesmo problema e todas as soluções do SO ou do Google não funcionaram para mim. Normalmente, setNeedsDisplayfunciona, mas quando não ...
Eu tentei chamar setNeedsDisplaydo modo de exibição de todas as maneiras possíveis de todos os threads possíveis e outras coisas - ainda sem sucesso. Nós sabemos, como Rob disse, que

"isso precisa ser desenhado no próximo ciclo de sorteio."

Mas por alguma razão não iria desenhar desta vez. E a única solução que encontrei é chamá-lo manualmente depois de algum tempo, para deixar passar qualquer coisa que bloqueie o empate, assim:

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

É uma boa solução se você não precisa que a visualização seja redesenhada com frequência. Caso contrário, se você estiver fazendo alguma coisa de movimento (ação), geralmente não há problemas em apenas pagar setNeedsDisplay.

Espero que ajude alguém que está perdido ali, como eu.

dreamzor
fonte
0

Bem, eu sei que isso pode ser uma grande mudança ou até mesmo não adequado para o seu projeto, mas você considerou não realizar o push até que já tenha os dados ? Dessa forma, você só precisa desenhar a vista uma vez e a experiência do usuário também será melhor - o push já estará carregado.

A maneira de fazer isso é UITableView didSelectRowAtIndexPathsolicitando os dados de maneira assíncrona. Depois de receber a resposta, você executa manualmente a segue e passa os dados para seu viewController no prepareForSegue. Enquanto isso, você pode querer mostrar algum indicador de atividade, para um indicador de carregamento simples, verifique https://github.com/jdg/MBProgressHUD

Miki
fonte