iOS: Verwenden von UIViews 'drawRect:' im Vergleich zu dem Delegierten seiner Ebene 'drawLayer: inContext:'

73

Ich habe eine Klasse, die eine Unterklasse von ist UIView. Ich bin in der Lage, Dinge in die Ansicht zu zeichnen, indem ich entweder die drawRectMethode implementiere oder drawLayer:inContext:eine Delegate-Methode von implementiere CALayer.

Ich habe zwei Fragen:

  1. Wie entscheide ich mich für einen Ansatz? Gibt es für jeden einen Anwendungsfall?
  2. Wenn ich implementiere drawLayer:inContext:, wird es aufgerufen (und drawRectist es nicht, zumindest soweit das Setzen eines Haltepunkts dies erkennen lässt), auch wenn ich meine Ansicht nicht als CALayerDelegat zuweise, indem ich Folgendes verwende:

    [[self layer] setDelegate:self];

    Wie kommt es, dass die Delegatmethode aufgerufen wird, wenn meine Instanz nicht als Delegat der Ebene definiert ist? und welcher Mechanismus verhindert, dass drawRecter aufgerufen wird, wenn er aufgerufen drawLayer:inContext:wird?

Itamar Katz
quelle

Antworten:

72

Wie entscheide ich mich für einen Ansatz? Gibt es für jeden einen Anwendungsfall?

Verwenden Sie immer drawRect:und niemals UIViewals Zeichnungsdelegierten für einen CALayer.

Wie kommt es, dass die Delegatmethode aufgerufen wird, wenn meine Instanz nicht als Delegat der Ebene definiert ist? und welcher Mechanismus verhindert, dass drawRect aufgerufen wird, wenn drawLayer:inContext:es aufgerufen wird?

Jede UIViewInstanz ist der Zeichnungsdelegierte für ihre Unterstützung CALayer. Deshalb [[self layer] setDelegate:self];schien nichts zu tun. Es ist überflüssig. Die drawRect:Methode ist effektiv die Zeichnungsdelegatmethode für die Ebene der Ansicht. Intern UIViewimplementiert, drawLayer:inContext:wo es einige seiner eigenen Sachen macht und dann aufruft drawRect:. Sie können es im Debugger sehen:

drawRect: Stacktrace

Aus diesem Grund drawRect:wurde bei der Implementierung nie aufgerufen drawLayer:inContext:. Aus diesem Grund sollten Sie niemals eine der CALayerZeichnungsdelegatmethoden in einer benutzerdefinierten UIViewUnterklasse implementieren . Sie sollten den Zeichnungsdelegierten auch niemals für eine andere Ebene anzeigen. Das wird allerlei Verrücktheit verursachen.

Wenn Sie implementieren, drawLayer:inContext:weil Sie auf das zugreifen müssen CGContextRef, können Sie dies aus Ihrem Inneren heraus drawRect:durch einen Anruf abrufen UIGraphicsGetCurrentContext().

Nathan Eror
quelle
Ich habe es als Teil des folgenden Codebeispiels von Apple implementiert AccelerometerGraph. Ich vermute, dass sie in diesem Codebeispiel implementiert wurden, drawLayer:inContext:da der Ebenenhierarchie neben der Stammschicht der Ansicht einige Ebenen hinzugefügt wurden und der Delegat tatsächlich nicht die UIViewInstanz ist.
Itamar Katz
3
developer.apple.com/library/ios/#DOCUMENTATION/WindowsViews/… "Es gibt auch andere Möglichkeiten, den Inhalt einer Ansicht bereitzustellen, z. B. das direkte Festlegen des Inhalts der zugrunde liegenden Ebene, aber das Überschreiben der drawRect: -Methode ist die häufigste Technik . "
Jamie
Hallo Nathan, ich bin sehr neu in der benutzerdefinierten Zeichnung in iOS. Ich habe eine untergeordnete UIView-Klasse, in der ich auf 3 verschiedenen Ebenen übereinander zeichnen müsste. Ich möchte die erste Schicht machen, etwas darauf zeichnen, dann die zweite Schicht auflegen und darauf zeichnen und dann die dritte. Wie erreiche ich das in der UIView-Unterklasse, die ich habe?
NSF
18
Dies ist keine richtige Antwort. Wann immer möglich, sollten wir CAlayer verwenden, anstatt drawRect zu überschreiben, um potenzielle Leistungsprobleme zu vermeiden. Weitere Informationen finden Sie in Apples WWDC 2012-Video - Leistung der iOS-App: Grafiken und Animationen (Link zur Website developer.apple.com/videos/wwdc/2012 ). Ansicht ab 11.40 Uhr, in der der Apple-Ingenieur dieses Szenario speziell angesprochen hat.
user2734323
3
Nathan, ich habe deine Aussage "Verwenden Sie immer drawRect:" kommentiert. Das ist nicht richtig. Apple empfiehlt die Verwendung von CALayer oder drawLayer: inContext, es sei denn, das Überschreiben von drawRect ist unbedingt erforderlich. Details dazu finden Sie im WWDC 2012-Video. Sie haben Recht, dass bestimmte Fälle wie das Zeichnen von Vektoren leicht durch Überschreiben von drawRect erreicht werden können (aber nicht die einzige Lösung). In den meisten Fällen bietet CALayer jedoch einen überlegenen Leistungsvorteil gegenüber drawRect. Daher sollte die Antwort "von Fall zu Fall" sein.
user2734323
45

drawRectsollte nur implementiert werden, wenn dies unbedingt erforderlich ist. Die Standardimplementierung von drawRectenthält eine Reihe intelligenter Optimierungen, z. B. das intelligente Zwischenspeichern des Renderings der Ansicht. Durch das Überschreiben werden all diese Optimierungen umgangen. Das ist schlecht. Durch die effektive Verwendung der Ebenenzeichnungsmethoden wird eine benutzerdefinierte Methode fast immer übertroffen drawRect. Apple verwendet a häufig UIViewals Delegat CALayer- tatsächlich ist jedes UIView der Delegat seiner Ebene . Sie können sehen, wie Sie die Ebenenzeichnung in einer UIView in mehreren Apple-Beispielen anpassen, einschließlich (zu diesem Zeitpunkt) ZoomingPDFViewer.

Während die Verwendung von drawRectüblich ist, ist es eine Praxis, von der seit mindestens 2002/2003, IIRC, abgeraten wird. Es gibt nicht mehr viele gute Gründe, diesen Weg zu gehen.

Erweiterte Leistungsoptimierung unter iPhone OS (Folie 15)

Grundlegende Grundlagen der Animation

Grundlegendes zum UIKit-Rendering

Technische Fragen und Antworten QA1708: Verbessern der Leistung beim Zeichnen von Bildern unter iOS

View Programming Guide: Optimieren der Ansichtszeichnung

quellish
quelle
Diese Antwort widerspricht der von @NathanEror akzeptierten Antwort. Es scheint mir, dass der Grund drawRect, der vermieden werden sollte, darin besteht, dass es jedes Mal aufgerufen wird, wenn die Ansicht aktualisiert wird, auch wenn sich nichts wirklich geändert hat. Wenn dies nicht der drawRectFall ist, verwendet Core Animation den vorhandenen Hintergrundspeicher der Ebene, der vermutlich früher gezeichnet wurde. Aber ich bin verzweifelt verwirrt und brauche ein offizielles Apple-Sequenzdiagramm ... irgendwelche Hinweise?
AlexChaffee
3
Siehe WWDC 2012-Sitzung "Leistung der iOS-App: Grafik und Animation". Das ist wahrscheinlich so nah wie möglich an dem, was Sie suchen. developer.apple.com/videos/wwdc/2012
quellish
6
OMG das ist eine tolle Präsentation! Ich bin gerade aufgestiegen ... Ich kann fühlen, wie meine Trefferpunkte und mein Mana zunehmen!
AlexChaffee
12

Hier sind die Codes von Sample ZoomingPDFViewer von 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
{
    ...
}
Dennis Fan
quelle
Falls jemand es nicht wirklich herausfinden kann, bedeutet der Kommentar, dass er wahrscheinlich die gesamte drawRect-Methode kompiliert, wenn sie definiert ist, aber nichts enthält. Es ist also dasselbe, als würde sie drawRect überhaupt nicht implementieren. Sie haben sie einfach so eingefügt, dass sie könnte erklären, warum Sie es nicht verwenden. Ein bisschen verwirrend für mich, aber ich habe die gesamte leere drawRect-Methode auskommentiert und das Programm funktioniert immer noch wie beabsichtigt (ZoomingPDFViewer)
mgrandi
7
Nein ... der Kommentar bedeutet, dass sich IF -drawRect: existiert, UIKit sich anders verhält (dh UIKit sieht aus, wenn die Ansicht auf Tselector drawRect: reagiert, und basierend darauf kann sein CALayer ungültig sein oder nicht).
Kalle
9

Ob Sie drawLayer(_:inContext:)oder drawRect(_:)(oder beide) für benutzerdefinierten Zeichnungscode verwenden, hängt davon ab, ob Sie während der Animation Zugriff auf den aktuellen Wert einer Ebeneneigenschaft benötigen.

Ich hatte heute Probleme mit verschiedenen Rendering-Problemen im Zusammenhang mit diesen beiden Funktionen, als ich meine eigene Label-Klasse implementierte . Nachdem ich die Dokumentation durchgesehen, einige Versuche und Irrtümer durchgeführt, UIKit dekompiliert und das Beispiel für benutzerdefinierte animierbare Eigenschaften von Apple überprüft hatte, bekam ich ein gutes Gefühl dafür, wie es funktioniert.

drawRect(_:)

Wenn Sie während der Animation nicht auf den aktuellen Wert einer Ebenen- / Ansichtseigenschaft zugreifen müssen, können Sie einfach drawRect(_:)Ihre benutzerdefinierte Zeichnung ausführen. Alles wird gut funktionieren.

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

drawLayer(_:inContext:)

Angenommen, Sie möchten backgroundColorin Ihrem benutzerdefinierten Zeichnungscode Folgendes verwenden:

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

Wenn Sie Ihren Code testen, werden Sie feststellen, dass backgroundColorwährend des Flugs einer Animation nicht der richtige (dh der aktuelle) Wert zurückgegeben wird. Stattdessen wird der endgültige Wert zurückgegeben (dh der Wert für den Abschluss der Animation).

Um den aktuellen Wert während der Animation zu erhalten, müssen Sie auf backgroundColorden layer Parameter zugreifen, an den übergeben wurde drawLayer(_:inContext:). Und Sie müssen auch auf den context Parameter zeichnen .

Es ist sehr wichtig zu wissen, dass die Ansicht self.layerund der übergebene layerParameter drawLayer(_:inContext:)nicht immer dieselbe Ebene sind! Letzteres könnte eine Kopie des ersteren sein, wobei teilweise Animationen bereits auf seine Eigenschaften angewendet wurden. Auf diese Weise können Sie auf korrekte Eigenschaftswerte von Animationen während des Flugs zugreifen.

Jetzt funktioniert die Zeichnung wie erwartet:

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

Es gibt jedoch zwei neue Probleme: setNeedsDisplay()und einige Eigenschaften wie backgroundColorund opaquefunktionieren für Ihre Ansicht nicht mehr. UIViewleitet Anrufe und Änderungen nicht mehr an die eigene Ebene weiter.

setNeedsDisplay()tut nur etwas, wenn Ihre Ansicht implementiert wird drawRect(_:). Es spielt keine Rolle, ob die Funktion tatsächlich etwas tut, aber UIKit verwendet sie, um zu bestimmen, ob Sie benutzerdefinierte Zeichnungen erstellen oder nicht.

Die Eigenschaften funktionieren wahrscheinlich nicht mehr, da UIViewdie eigene Implementierung von drawLayer(_:inContext:)nicht mehr aufgerufen wird.

Die Lösung ist also ganz einfach. Rufen Sie einfach die Implementierung der Superklasse auf drawLayer(_:inContext:)und implementieren Sie eine leere drawRect(_:):

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.
}

Zusammenfassung

Verwenden drawRect(_:)Sie diese Option , solange Sie nicht das Problem haben, dass Eigenschaften während einer Animation falsche Werte zurückgeben:

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

Verwenden Sie drawLayer(_:inContext:) und, drawRect(_:) wenn Sie auf den aktuellen Wert der Ansichts- / Ebeneneigenschaften zugreifen müssen, während diese animiert werden:

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.
}
Fluidsonic
quelle
7

Unter iOS ist die Überlappung zwischen einer Ansicht und ihrer Ebene sehr groß. Standardmäßig ist die Ansicht der Delegat ihrer Ebene und implementiert die drawLayer:inContext:Methode der Ebene . So wie ich es verstehe drawRect:und drawLayer:inContext:in diesem Fall mehr oder weniger gleichwertig bin . Möglicherweise wird die Standardimplementierung von drawLayer:inContext:Aufrufen drawRect:oder drawRect:nur aufgerufen, wenn sie drawLayer:inContext:nicht von Ihrer Unterklasse implementiert wird.

Wie entscheide ich mich für einen Ansatz? Gibt es für jeden einen Anwendungsfall?

Es ist nicht wirklich wichtig. Um der Konvention zu folgen, würde ich normalerweise die Verwendung verwenden drawRect:und reservieren, drawLayer:inContext:wenn ich tatsächlich benutzerdefinierte Unterschichten zeichnen muss, die nicht Teil einer Ansicht sind.

Ole Begemann
quelle
Du hast mich in Sekundenschnelle geschlagen. :-P
Nathan
1
@ Nathan: Aber deine Antwort ist besser als meine.
Ole Begemann
Hallo Ole, ich bin sehr neu in der benutzerdefinierten Zeichnung in iOS. Ich habe eine untergeordnete UIView-Klasse, in der ich auf 3 verschiedenen Ebenen übereinander zeichnen müsste. Ich möchte die erste Schicht machen, etwas darauf zeichnen, dann die zweite Schicht auflegen und darauf zeichnen und dann die dritte. Wie erreiche ich das in der UIView-Unterklasse, die ich habe?
NSF
Die beiden Funktionen dienen unterschiedlichen Zwecken. Überprüfen Sie meine Antwort unten: stackoverflow.com/a/36050120/1183577
fluidsonic
2

In der Apple-Dokumentation heißt es: "Es gibt auch andere Möglichkeiten, den Inhalt einer Ansicht bereitzustellen, z. B. das direkte Festlegen des Inhalts der zugrunde liegenden Ebene, aber das Überschreiben der drawRect: -Methode ist die häufigste Technik."

Aber es geht nicht auf Details ein, also sollte das ein Hinweis sein: Tu es nicht, es sei denn, du willst dir wirklich die Hände schmutzig machen.

Der Delegat der UIView-Ebene zeigt auf die UIView. Das UIView verhält sich jedoch unterschiedlich, je nachdem, ob drawRect: implementiert ist oder nicht. Wenn Sie beispielsweise die Eigenschaften auf der Ebene direkt festlegen (z. B. die Hintergrundfarbe oder den Eckenradius), werden diese Werte überschrieben, wenn Sie eine drawRect: -Methode haben - auch wenn diese vollständig leer ist (dh nicht einmal super aufruft).

Jamie
quelle