web-dev-qa-db-fra.com

iOS: Utilisation de 'drawRect:' d'UIView par rapport au délégué de sa couche 'drawLayer: inContext:'

J'ai une classe qui est une sous-classe de UIView. Je peux dessiner des trucs à l'intérieur de la vue soit en implémentant la méthode drawRect, soit en implémentant drawLayer:inContext: qui est une méthode déléguée de CALayer.

J'ai deux questions:

  1. Comment décider quelle approche utiliser? Existe-t-il un cas d'utilisation pour chacun?
  2. Si j'implémente drawLayer:inContext:, il est appelé (et drawRect ne l'est pas, du moins en ce qui concerne la mise en place d'un point d'arrêt), même si je n'affecte pas ma vue en tant que délégué CALayer en utilisant :

    [[self layer] setDelegate:self];

    comment se fait-il que la méthode déléguée soit appelée si mon instance n'est pas définie comme déléguée de la couche? et quel mécanisme empêche drawRect d'être appelé si drawLayer:inContext: est appelé?

71
Itamar Katz

Comment décider quelle approche utiliser? Existe-t-il un cas d'utilisation pour chacun?

Utilisez toujours drawRect: Et n'utilisez jamais un UIView comme délégué de dessin pour un CALayer.

comment se fait-il que la méthode déléguée soit appelée si mon instance n'est pas définie comme déléguée de la couche? et quel mécanisme empêche drawRect d'être appelé si drawLayer:inContext: est appelé?

Chaque instance de UIView est le délégué de dessin pour son support CALayer. C'est pourquoi [[self layer] setDelegate:self]; Semblait ne rien faire. C'est redondant. La méthode drawRect: Est en fait la méthode de délégué de dessin pour le calque de la vue. En interne, UIView implémente drawLayer:inContext: Où il fait ses propres trucs et appelle ensuite drawRect:. Vous pouvez le voir dans le débogueur:

drawRect: stacktrace

C'est pourquoi drawRect: N'a jamais été appelé lorsque vous avez implémenté drawLayer:inContext:. C'est également la raison pour laquelle vous ne devez jamais implémenter l'une des méthodes de délégation de dessin CALayer dans une sous-classe UIView personnalisée. Vous ne devez également jamais faire de la vue le délégué de dessin pour un autre calque. Cela provoquera toutes sortes de délires.

Si vous implémentez drawLayer:inContext: Parce que vous devez accéder au CGContextRef, vous pouvez l'obtenir depuis l'intérieur de votre drawRect: En appelant UIGraphicsGetCurrentContext().

72
Nathan Eror

drawRect ne doit être implémenté qu'en cas de nécessité absolue. L'implémentation par défaut de drawRect comprend un certain nombre d'optimisations intelligentes, comme la mise en cache intelligente du rendu de la vue. La neutralisation contourne toutes ces optimisations. C'est mauvais. L'utilisation efficace des méthodes de dessin de calque surpassera presque toujours un drawRect personnalisé. Apple utilise un UIView comme délégué pour un CALayer souvent - en fait, tous les IView est le délégué de sa couche . Vous pouvez voir comment personnaliser le dessin de calque à l'intérieur d'une UIView dans plusieurs exemples Apple Apple incluant (pour le moment) ZoomingPDFViewer.

Bien que l'utilisation de drawRect soit courante, c'est une pratique qui est découragée depuis au moins 2002/2003, IIRC. Il n'y a pas beaucoup de bonnes raisons de s'engager dans cette voie.

Optimisation avancée des performances sur iPhone OS (diapositive 15)

Essentiels d'animation de base

Comprendre le rendu UIKit

Q&A technique QA1708: Amélioration des performances de dessin d'images sur iOS

Guide de programmation des vues: optimisation du dessin de la vue

44
quellish

Voici les codes de l'exemple ZoomingPDFViewer d'Apple:

-(void)drawRect:(CGRect)r
{

    // UIView uses the existence of -drawRect: to determine if it should allow its CALayer
    // to be invalidated, which would then lead to the layer creating a backing store and
    // -drawLayer:inContext: being called.
    // By implementing an empty -drawRect: method, we allow UIKit to continue to implement
    // this logic, while doing our real drawing work inside of -drawLayer:inContext:

}

-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
    ...
}
12
Dennis Fan

Que vous utilisiez drawLayer(_:inContext:) ou drawRect(_:) (ou les deux) pour le code de dessin personnalisé dépend si vous avez besoin d'accéder à la valeur actuelle d'une propriété de calque pendant il est en cours d'animation.

Je luttais aujourd'hui avec divers problèmes de rendu liés à ces deux fonctions lors de l'implémentation ma propre classe Label . Après avoir vérifié la documentation, fait quelques essais et erreurs, décompilé UIKit et inspecté Apple exemple de propriétés animables personnalisées j'ai eu une bonne idée de la façon dont cela fonctionne.

drawRect(_:)

Si vous n'avez pas besoin d'accéder à la valeur actuelle d'une propriété de calque/vue pendant son animation, vous pouvez simplement utiliser drawRect(_:) pour effectuer votre dessin personnalisé. Tout fonctionnera très bien.

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

drawLayer(_:inContext:)

Imaginons par exemple que vous souhaitiez utiliser backgroundColor dans votre code de dessin personnalisé:

override func drawRect(rect: CGRect) {
    let colorForCustomDrawing = self.layer.backgroundColor
    // your custom drawing code
}

Lorsque vous testez votre code, vous remarquerez que backgroundColor ne renvoie pas la valeur correcte (c'est-à-dire la valeur actuelle) pendant qu'une animation est en cours. Au lieu de cela, il renvoie la valeur finale (c'est-à-dire la valeur de la fin de l'animation).

Pour obtenir la valeur actuelle pendant l'animation, vous devez accéder au backgroundColor du layer paramètre passé à drawLayer(_:inContext:). Et vous devez également dessiner sur le paramètre context.

Il est très important de savoir que self.layer Et le paramètre layer d'une vue passé à drawLayer(_:inContext:) ne sont pas toujours le même calque! Ce dernier peut être une copie du premier avec des animations partielles déjà appliquées à ses propriétés. De cette façon, vous pouvez accéder aux valeurs de propriété correctes des animations en vol.

Maintenant, le dessin fonctionne comme prévu:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}

Mais il y a deux nouveaux problèmes: setNeedsDisplay() et plusieurs propriétés comme backgroundColor et opaque ne fonctionnent plus pour votre vue. UIView ne transfère plus les appels et passe à sa propre couche.

setNeedsDisplay() ne fait quelque chose que si votre vue implémente drawRect(_:). Peu importe si la fonction fait réellement quelque chose, mais UIKit l'utilise pour déterminer si vous faites ou non un dessin personnalisé.

Les propriétés ne fonctionnent probablement plus car la propre implémentation de UIView de drawLayer(_:inContext:) n'est plus appelée.

La solution est donc assez simple. Appelez simplement l'implémentation de la superclasse de drawLayer(_:inContext:) et implémentez une drawRect(_:) vide:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}

Sommaire

Utilisez drawRect(_:) tant que vous n'avez pas le problème que les propriétés retournent des valeurs incorrectes pendant une animation:

override func drawRect(rect: CGRect) {
    // your custom drawing code
}

Utilisez drawLayer(_:inContext:) et drawRect(_:) si vous devez accéder à la valeur actuelle des propriétés de la vue/couche pendant qu'elles sont en cours d'animation:

override func drawLayer(layer: CALayer, inContext context: CGContext) {
    super.drawLayer(layer, inContext: context)

    let colorForCustomDrawing = layer.backgroundColor
    // your custom drawing code
}


override func drawRect(rect: CGRect) {
    // Although we use drawLayer(_:inContext:) we still need to implement this method.
    // UIKit checks for its presence when it decides whether a call to setNeedsDisplay() is forwarded to its layer.
}
9
fluidsonic

Sur iOS, le chevauchement entre une vue et sa couche est très important. Par défaut, la vue est le délégué de son calque et implémente le drawLayer:inContext: méthode. Si je comprends bien, drawRect: et drawLayer:inContext: sont plus ou moins équivalents dans ce cas. Il est possible que l'implémentation par défaut de drawLayer:inContext: appels drawRect:, ou drawRect: n'est appelé que si drawLayer:inContext: n'est pas implémenté par votre sous-classe.

Comment décider quelle approche utiliser? Existe-t-il un cas d'utilisation pour chacun?

Ça n'a pas vraiment d'importance. Pour suivre la convention, j'utiliserais normalement drawRect: et réserver l'utilisation de drawLayer:inContext: lorsque je dois réellement dessiner des sous-couches personnalisées qui ne font pas partie d'une vue.

7
Ole Begemann

Documentation Apple a ceci à dire: "Il existe également d'autres façons de fournir le contenu d'une vue, comme définir directement le contenu de la couche sous-jacente, mais remplacer la méthode drawRect: est la technique la plus courante. "

Mais cela n'entre pas dans les détails, donc ça devrait être un indice: ne le faites pas sauf si vous voulez vraiment vous salir les mains.

Le délégué de la couche UIView pointe vers l'UIView. Cependant, UIView se comporte différemment selon que drawRect: est implémenté ou non. Par exemple, si vous définissez directement les propriétés du calque (comme sa couleur d'arrière-plan ou son rayon de coin), ces valeurs sont écrasées si vous avez une méthode drawRect: - même si elle est complètement vide (c'est-à-dire qu'elle n'appelle même pas super).

2
jamie

Pour les vues à couches avec contenu personnalisé, vous devez continuer à remplacer les méthodes de la vue pour réaliser votre dessin. Une vue basée sur des couches se fait automatiquement déléguée de sa couche et implémente les méthodes de délégué nécessaires, et vous ne devez pas modifier cette configuration. Au lieu de cela, vous devez implémenter la méthode drawRect: de votre vue pour dessiner votre contenu . Core Animation Programming Guide

0
Singer Quincy