Quelle est la manière la plus robuste de forcer un UIView à se redessiner?

117

J'ai un UITableView avec une liste d'articles. La sélection d'un élément pousse un viewController qui procède ensuite aux opérations suivantes. from method viewDidLoad Je déclenche une URLRequest pour les données requises par l'une de mes sous-vues - une sous-classe UIView avec drawRect remplacé. Lorsque les données arrivent du cloud, je commence à construire ma hiérarchie de vues. la sous-classe en question reçoit les données et sa méthode drawRect a maintenant tout ce dont elle a besoin pour le rendu.

Mais.

Parce que je n'appelle pas drawRect explicitement - Cocoa-Touch gère cela - je n'ai aucun moyen d'informer Cocoa-Touch que je veux vraiment, vraiment que cette sous-classe UIView soit rendue. Quand? Ce serait bien maintenant!

J'ai essayé [myView setNeedsDisplay]. Cela fonctionne un peu parfois. Très irrégulier.

Je lutte avec ça depuis des heures et des heures. Quelqu'un pourrait-il s'il vous plaît me fournir une approche solide et garantie pour forcer un nouveau rendu UIView.

Voici l'extrait de code qui fournit des données à la vue:

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

Salut, Doug

dugla
la source

Réponses:

193

Le moyen sûr et solide de forcer un UIViewà refaire le rendu est [myView setNeedsDisplay]. Si vous rencontrez des problèmes avec cela, vous rencontrez probablement l'un de ces problèmes:

  • Vous l'appelez avant d'avoir réellement les données, ou vous mettez -drawRect:quelque chose en cache.

  • Vous vous attendez à ce que la vue se dessine au moment où vous appelez cette méthode. Il n'y a intentionnellement aucun moyen d'exiger «dessiner maintenant cette seconde» en utilisant le système de dessin Cocoa. Cela perturberait l'ensemble du système de composition de vues, détruirait les performances et créerait probablement toutes sortes d'artefacts. Il n'y a que des moyens de dire "cela doit être dessiné lors du prochain cycle de tirage".

Si ce dont vous avez besoin est "un peu de logique, dessiner, un peu plus de logique", alors vous devez mettre le "un peu plus de logique" dans une méthode séparée et l'invoquer -performSelector:withObject:afterDelay:avec un délai de 0. Cela mettra "un peu plus de logique" après le prochain cycle de tirage. Voir cette question pour un exemple de ce type de code et un cas où cela pourrait être nécessaire (bien qu'il soit généralement préférable de rechercher d'autres solutions si possible car cela complique le code).

Si vous ne pensez pas que les choses se dessinent, mettez un point d'arrêt -drawRect:et voyez quand vous êtes appelé. Si vous appelez -setNeedsDisplay, mais que vous n'êtes -drawRect:pas appelé dans la boucle d'événements suivante, fouillez dans votre hiérarchie de vues et assurez-vous que vous n'essayez pas de déjouer quelque part. L'excès d'intelligence est la première cause de mauvais dessin selon mon expérience. Lorsque vous pensez savoir comment tromper le système pour qu'il fasse ce que vous voulez, vous le faites généralement faire exactement ce que vous ne voulez pas.

Rob Napier
la source
Rob, voici ma liste de contrôle. 1) Ai-je les données? Oui. Je crée la vue dans une méthode - hideSpinner - appelée sur le thread principal depuis le connectionDidFinishLoading: ainsi: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Je n'ai pas besoin de dessiner instantanément. J'en ai juste besoin. Aujourd'hui. Actuellement, c'est complètement aléatoire et hors de mon contrôle. [myView setNeedsDisplay] n'est pas du tout fiable. Je suis même allé jusqu'à appeler [myView setNeedsDisplay] dans viewDidAppear :. Nuthin '. Cocoa m'ignore tout simplement. Exaspérant!!
dugla
1
J'ai constaté qu'avec cette approche, il y a souvent un délai entre l' setNeedsDisplayappel et l'appel réel de drawRect:. Bien que la fiabilité des appels soit là, je n'appellerais pas cela la solution «la plus robuste» - une solution de dessin robuste devrait, en théorie, faire tout le dessin immédiatement avant de revenir au code client qui demande le tirage au sort.
Slipp D.Thompson
1
J'ai commenté ci-dessous votre réponse avec plus de détails. Essayer de forcer le système de dessin à fonctionner dans le désordre en appelant des méthodes que la documentation dit explicitement de ne pas appeler n'est pas robuste. Au mieux, il s'agit d'un comportement indéfini. Très probablement, cela dégradera les performances et la qualité du dessin. Au pire, cela pourrait se bloquer ou s'écraser.
Rob Napier
1
Si vous avez besoin de dessiner avant de revenir, dessinez dans un contexte d'image (qui fonctionne exactement comme le canevas que beaucoup de gens attendent). Mais n'essayez pas de tromper le système de composition. Il est conçu pour fournir des graphiques de haute qualité très rapidement avec un minimum de surcharge de ressources. Une partie de cela est la fusion des étapes de dessin à la fin de la boucle d'exécution.
Rob Napier
1
@RobNapier Je ne comprends pas pourquoi vous devenez si défensif. Veuillez vérifier à nouveau; il n'y a pas de violation d'API (bien qu'il ait été nettoyé en fonction de votre suggestion, je dirais que le fait d'appeler sa propre méthode n'est pas une violation), ni de risque de blocage ou de panne. Ce n'est pas non plus un «truc»; il utilise setNeedsDisplay / displayIfNeeded de CALayer de manière normale. De plus, je l'utilise depuis un certain temps maintenant pour dessiner avec Quartz d'une manière parallèle à GLKView / OpenGL; il s'est avéré sûr, stable et plus rapide que la solution que vous avez répertoriée. Si vous ne me croyez pas, essayez-le. Vous n'avez rien à perdre ici.
Slipp D.Thompson
52

J'ai eu un problème avec un grand délai entre l'appel de setNeedsDisplay et drawRect: (5 secondes). Il s'est avéré que j'ai appelé setNeedsDisplay dans un thread différent du thread principal. Après avoir déplacé cet appel vers le thread principal, le délai a disparu.

J'espère que cela aidera.

Werner Altewischer
la source
C'était définitivement mon bug. J'avais l'impression que je courais sur le thread principal, mais ce n'est que lorsque j'ai NSLogged NSThread.isMainThread que j'ai réalisé qu'il y avait un cas d'angle où je n'effectuais PAS les changements de couche sur le thread principal. Merci de m'avoir évité de m'arracher les cheveux!
M. T
14

La manière garantie de remboursement, en béton armé solide pour forcer une vue à dessiner de manière synchrone (avant de revenir au code appelant) est de configurer les CALayerinteractions de la avec votre UIViewsous - classe.

Dans votre sous-classe UIView, créez une displayNow()méthode qui indique au calque de « définir le cap pour l'affichage » puis de « faire en sorte »:

Rapide

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

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

Implémentez également une draw(_: CALayer, in: CGContext)méthode qui appellera votre méthode de dessin privée / interne (qui fonctionne puisque tout UIViewest a CALayerDelegate) :

Rapide

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

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

Et créez votre internalDraw(_: CGRect)méthode personnalisée , avec une sécurité intégrée draw(_: CGRect):

Rapide

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

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

Et maintenant, il suffit d'appeler myView.displayNow()chaque fois que vous en avez vraiment besoin pour dessiner (par exemple à partir d'un CADisplayLinkrappel) . Notre displayNow()méthode indiquera CALayerà displayIfNeeded(), qui rappellera de manière synchrone notre draw(_:,in:)et effectuera le dessin internalDraw(_:), mettant à jour le visuel avec ce qui est dessiné dans le contexte avant de continuer.


Cette approche est similaire à celle de @ RobNapier ci-dessus, mais présente l'avantage d'appeler displayIfNeeded()en plus de setNeedsDisplay(), ce qui la rend synchrone.

Cela est possible parce que CALayers exposent plus de fonctionnalités de dessin que ne le font UIView- les couches sont de niveau inférieur aux vues et conçues explicitement dans le but de dessiner hautement configurable dans la mise en page et (comme beaucoup de choses dans Cocoa) sont conçues pour être utilisées de manière flexible ( en tant que classe parente, ou en tant que délégant, ou en tant que pont vers d'autres systèmes de dessin, ou simplement seuls). Une bonne utilisation du CALayerDelegateprotocole rend tout cela possible.

Pour plus d'informations sur la configurabilité de CALayers, reportez-vous à la section Configuration des objets de couche du Guide de programmation de l'animation de base .

Slipp D. Thompson
la source
Notez que la documentation de drawRect:dit explicitement "Vous ne devez jamais appeler cette méthode directement vous-même." En outre, CALayer displaydit explicitement " N'appelez pas cette méthode directement." Si vous souhaitez dessiner contentsdirectement sur les calques de manière synchrone, il n'est pas nécessaire de violer ces règles. Vous pouvez simplement dessiner sur le calque à contentstout moment (même sur les fils d'arrière-plan). Ajoutez simplement une sous-couche à la vue pour cela. Mais c'est différent de le mettre à l'écran, qui doit attendre le bon moment de composition.
Rob Napier
Je dis d'ajouter un sous-calque car la documentation avertit également de ne pas jouer directement avec le calque d'un UIView contents("Si l'objet de calque est lié à un objet de vue, vous devez éviter de définir le contenu de cette propriété directement. L'interaction entre les vues et les calques résulte généralement dans la vue remplaçant le contenu de cette propriété lors d'une mise à jour ultérieure. ") Je ne recommande pas particulièrement cette approche; un dessin prématuré nuit aux performances et à la qualité du dessin. Mais si vous en avez besoin pour une raison quelconque, contentscomment l'obtenir.
Rob Napier
@RobNapier Point pris en appelant drawRect:directement. Il n'était pas nécessaire de démontrer cette technique et a été corrigé.
Slipp D.Thompson
@RobNapier Quant à la contentstechnique que vous suggérez… semble séduisante. J'ai essayé quelque chose comme ça au départ, mais je n'ai pas pu le faire fonctionner, et j'ai trouvé que la solution ci-dessus était beaucoup moins de code sans aucune raison de ne pas être aussi performante. Cependant, si vous avez une solution de travail pour l' contentsapproche, je serais intéressé à la lire (il n'y a aucune raison pour laquelle vous ne pouvez pas avoir deux réponses à cette question, non?)
Slipp D.Thompson
@RobNapier Remarque: cela n'a rien à voir avec CALayer display. C'est juste une méthode publique personnalisée dans une sous-classe UIView, tout comme ce qui est fait dans GLKView (une autre sous-classe UIView, et la seule écrite par Apple que je connaisse qui nécessite la fonctionnalité DRAW NOW! ).
Slipp D.Thompson
5

J'ai eu le même problème et toutes les solutions de SO ou de Google ne fonctionnaient pas pour moi. Habituellement, setNeedsDisplaycela fonctionne, mais quand ce n'est pas le cas ...
J'ai essayé d'appeler setNeedsDisplayla vue de toutes les manières possibles à partir de tous les threads et trucs possibles - toujours sans succès. Nous savons, comme Rob l'a dit, que

"cela doit être dessiné dans le prochain cycle de tirage."

Mais pour une raison quelconque, il ne dessinerait pas cette fois. Et la seule solution que j'ai trouvée est de l'appeler manuellement après un certain temps, pour laisser passer tout ce qui bloque le tirage au sort, comme ceci:

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

C'est une bonne solution si vous n'avez pas besoin que la vue soit redessinée très souvent. Sinon, si vous faites des choses en mouvement (action), il n'y a généralement aucun problème avec le simple appel setNeedsDisplay.

J'espère que cela aidera quelqu'un qui est perdu là-bas, comme moi.

dreamzor
la source
0

Eh bien, je sais que cela pourrait être un grand changement ou même pas adapté à votre projet, mais avez-vous envisagé de ne pas effectuer le push tant que vous n'avez pas déjà les données ? De cette façon, vous n'avez besoin de dessiner la vue qu'une seule fois et l'expérience utilisateur sera également meilleure - le push se déplacera déjà chargé.

La façon dont vous faites cela consiste à UITableView didSelectRowAtIndexPathdemander les données de manière asynchrone. Une fois que vous recevez la réponse, vous effectuez manuellement la transition et transmettez les données à votre viewController dans prepareForSegue. En attendant, vous voudrez peut-être afficher un indicateur d'activité, pour un simple indicateur de chargement, vérifiez https://github.com/jdg/MBProgressHUD

Miki
la source