Was ist die robusteste Methode, um ein UIView zum Neuzeichnen zu zwingen?

116

Ich habe eine UITableView mit einer Liste von Elementen. Durch Auswahl eines Elements wird ein viewController verschoben, der dann die folgenden Schritte ausführt. Von der Methode viewDidLoad Ich starte eine URLRequest für Daten, die von einer meiner Unteransichten benötigt werden - eine UIView-Unterklasse mit überschriebenem drawRect. Wenn die Daten aus der Cloud ankommen, beginne ich mit dem Aufbau meiner Ansichtshierarchie. Die betreffende Unterklasse erhält die Daten und die drawRect-Methode verfügt nun über alles, was zum Rendern erforderlich ist.

Aber.

Da ich drawRect nicht explizit aufrufe - Cocoa-Touch übernimmt das -, kann ich Cocoa-Touch nicht darüber informieren, dass ich wirklich, wirklich möchte, dass diese UIView-Unterklasse gerendert wird. Wann? Jetzt wäre gut!

Ich habe [myView setNeedsDisplay] ausprobiert. Das funktioniert manchmal irgendwie. Sehr fleckig.

Ich habe stundenlang damit gerungen. Könnte jemand, der mir bitte einen soliden, garantierten Ansatz bietet, um ein UIView-Re-Rendering zu erzwingen.

Hier ist der Codeausschnitt, der der Ansicht Daten zuführt:

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

Prost, Doug

Dugla
quelle

Antworten:

192

Die garantierte, absolut solide Möglichkeit, ein UIViewerneutes Rendern zu erzwingen , ist [myView setNeedsDisplay]. Wenn Sie Probleme damit haben, stoßen Sie wahrscheinlich auf eines der folgenden Probleme:

  • Sie rufen es auf, bevor Sie die Daten tatsächlich haben, oder Sie -drawRect:überspeichern etwas.

  • Sie erwarten, dass die Ansicht in dem Moment gezeichnet wird, in dem Sie diese Methode aufrufen. Es gibt absichtlich keine Möglichkeit, mit dem Cocoa-Zeichensystem "jetzt genau in dieser Sekunde zeichnen" zu fordern. Dies würde das gesamte View-Compositing-System und die Leistung des Papierkorbs stören und wahrscheinlich alle Arten von Artefakten verursachen. Es gibt nur Möglichkeiten zu sagen, "dies muss im nächsten Ziehzyklus gezeichnet werden."

Wenn Sie "etwas Logik, Zeichnen, etwas mehr Logik" benötigen, müssen Sie "etwas mehr Logik" in eine separate Methode einfügen und -performSelector:withObject:afterDelay:mit einer Verzögerung von 0 aufrufen. Dadurch wird "etwas mehr Logik" nachher eingefügt der nächste Ziehzyklus. In dieser Frage finden Sie ein Beispiel für diese Art von Code und einen Fall, in dem er möglicherweise benötigt wird (obwohl es normalerweise am besten ist, nach anderen Lösungen zu suchen, wenn dies möglich ist, da dies den Code kompliziert).

Wenn Sie nicht glauben, dass Dinge gezeichnet werden, setzen Sie einen Haltepunkt -drawRect:und sehen Sie, wann Sie angerufen werden. Wenn Sie anrufen -setNeedsDisplay, aber -drawRect:in der nächsten Ereignisschleife nicht aufgerufen werden, stöbern Sie in Ihrer Ansichtshierarchie und stellen Sie sicher, dass Sie nicht versuchen, irgendwo zu überlisten. Über-Cleverness ist meiner Erfahrung nach die häufigste Ursache für schlechtes Zeichnen. Wenn Sie glauben, am besten zu wissen, wie Sie das System dazu bringen können, das zu tun, was Sie wollen, wird es normalerweise genau das tun, was Sie nicht wollen.

Rob Napier
quelle
Rob, hier ist meine Checkliste. 1) Habe ich die Daten? Ja. Ich erstelle die Ansicht in einer Methode - hideSpinner -, die im Hauptthread von connectionDidFinishLoading aufgerufen wird: also: [self performSelectorOnMainThread: @selector (hideSpinner) withObject: nil waitUntilDone: NO]; 2) Ich muss nicht sofort zeichnen. Ich brauche es einfach. Heute. Derzeit ist es völlig zufällig und außerhalb meiner Kontrolle. [myView setNeedsDisplay] ist völlig unzuverlässig. Ich bin sogar so weit gegangen, [myView setNeedsDisplay] in viewDidAppear: aufzurufen. Nuthin '. Kakao ignoriert mich einfach. Wahnsinn !!
Dugla
1
Ich habe festgestellt, dass bei diesem Ansatz häufig eine Verzögerung zwischen dem setNeedsDisplayund dem tatsächlichen Anruf von auftritt drawRect:. Während die Anrufzuverlässigkeit hier ist, würde ich dies nicht als die „robusteste“ Lösung bezeichnen - eine robuste Zeichnungslösung sollte theoretisch die gesamte Zeichnung unmittelbar vor der Rückkehr zum Client-Code ausführen, der die Zeichnung erfordert.
Slipp D. Thompson
1
Ich habe Ihre Antwort unten mit weiteren Details kommentiert. Der Versuch, das Zeichnungssystem durch Aufrufen von Methoden zum Auslaufen zu bringen, ist in der Dokumentation nicht robust. Bestenfalls ist es undefiniertes Verhalten. Höchstwahrscheinlich wird dies die Leistung und die Zeichenqualität beeinträchtigen. Im schlimmsten Fall kann es zum Stillstand kommen oder abstürzen.
Rob Napier
1
Wenn Sie vor der Rückkehr zeichnen müssen, zeichnen Sie in einen Bildkontext (der genau wie die Leinwand funktioniert, die viele Leute erwarten). Aber versuchen Sie nicht, das Compositing-System auszutricksen. Es wurde entwickelt, um sehr schnell qualitativ hochwertige Grafiken mit einem Minimum an Ressourcenaufwand bereitzustellen. Ein Teil davon ist das Zusammenführen von Zeichenschritten am Ende der Laufschleife.
Rob Napier
1
@RobNapier Ich verstehe nicht, warum du so defensiv wirst. Bitte überprüfen Sie noch einmal; Es gibt keine API-Verletzung (obwohl sie aufgrund Ihres Vorschlags bereinigt wurde, würde ich behaupten, dass das Aufrufen der eigenen Methode keine Verletzung darstellt) oder die Möglichkeit eines Deadlocks oder Absturzes. Es ist auch kein "Trick"; Es verwendet CALayers setNeedsDisplay / displayIfNeeded auf normale Weise. Außerdem benutze ich es schon eine ganze Weile, um parallel zu GLKView / OpenGL mit Quarz zu zeichnen. Es hat sich als sicher, stabil und schneller als die von Ihnen aufgeführte Lösung erwiesen. Wenn Sie mir nicht glauben, versuchen Sie es. Sie haben hier nichts zu verlieren.
Slipp D. Thompson
52

Ich hatte ein Problem mit einer großen Verzögerung zwischen dem Aufruf von setNeedsDisplay und drawRect: (5 Sekunden). Es stellte sich heraus, dass ich setNeedsDisplay in einem anderen Thread als dem Hauptthread aufgerufen habe. Nachdem dieser Aufruf in den Haupt-Thread verschoben wurde, verschwand die Verzögerung.

Hoffe das ist eine Hilfe.

Werner Altewischer
quelle
Das war definitiv mein Fehler. Ich hatte den Eindruck, dass ich auf dem Hauptthread lief, aber erst als ich NSThread.isMainThread mit NSLoggte, stellte ich fest, dass es einen Eckfall gab, in dem ich die Ebenenänderungen am Hauptthread NICHT vornahm. Danke, dass du mich davor bewahrt hast, mir die Haare auszureißen!
Herr T
14

Die Geld-zurück-garantierte, Stahlbeton-solide Methode, um eine Ansicht zum synchronen Zeichnen zu zwingen (bevor Sie zum aufrufenden Code zurückkehren), besteht darin, die CALayerInteraktionen des UIViewBenutzers mit Ihrer Unterklasse zu konfigurieren .

Erstellen Sie in Ihrer UIView-Unterklasse eine displayNow()Methode, die der Ebene sagt, dass sie " Kurs für die Anzeige festlegen " und dann "so machen " soll:

Schnell

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

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

Implementieren Sie auch eine draw(_: CALayer, in: CGContext)Methode, die Ihre private / interne Zeichenmethode aufruft (was funktioniert, da jede a UIViewist CALayerDelegate) :

Schnell

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

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

Und erstellen Sie Ihre benutzerdefinierte internalDraw(_: CGRect)Methode zusammen mit ausfallsicher draw(_: CGRect):

Schnell

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

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

Und jetzt rufen myView.displayNow()Sie einfach an, wann immer Sie es wirklich brauchen, um zu zeichnen (z. B. von einem CADisplayLinkRückruf) . Unsere displayNow()Methode teilt das CALayerto mit displayIfNeeded(), das synchron in unser zurückruft draw(_:,in:)und das Zeichnen internalDraw(_:)vornimmt, und aktualisiert das Bild mit dem, was in den Kontext gezeichnet wird, bevor Sie fortfahren.


Dieser Ansatz ähnelt dem oben beschriebenen von @ RobNapier, hat jedoch den Vorteil, dass displayIfNeeded()zusätzlich aufgerufen wird setNeedsDisplay(), wodurch er synchron ist.

Dies ist möglich, weil CALayers mehr Zeichenfunktionen UIViewbieten als s - Ebenen sind niedriger als Ansichten und wurden explizit für den Zweck einer hoch konfigurierbaren Zeichnung innerhalb des Layouts entworfen und (wie viele Dinge in Cocoa) so konzipiert, dass sie flexibel verwendet werden können ( als Elternklasse oder als Delegator oder als Brücke zu anderen Zeichensystemen oder nur für sich allein). Die ordnungsgemäße Verwendung des CALayerDelegateProtokolls macht dies alles möglich.

Weitere Informationen zur Konfigurierbarkeit von CALayers finden Sie im Abschnitt Einrichten von Ebenenobjekten im Core Animation Programming Guide .

Slipp D. Thompson
quelle
Beachten Sie, dass in der Dokumentation zu drawRect:explizit angegeben ist: "Sie sollten diese Methode niemals direkt selbst aufrufen." Außerdem displaysagt CALayer ausdrücklich: "Rufen Sie diese Methode nicht direkt auf." Wenn Sie contentsdirekt auf den Ebenen synchron zeichnen möchten, müssen Sie diese Regeln nicht verletzen. Sie können contentsjederzeit auf die Ebenen zeichnen (auch auf Hintergrund-Threads). Fügen Sie dazu einfach eine Unterebene zur Ansicht hinzu. Dies unterscheidet sich jedoch von der Anzeige auf dem Bildschirm, bei der auf die richtige Compositing-Zeit gewartet werden muss.
Rob Napier
Ich möchte eine Unterebene hinzufügen, da die Dokumente auch davor warnen, direkt mit den Ebenen eines UIView zu spielen contents("Wenn das Ebenenobjekt an ein Ansichtsobjekt gebunden ist, sollten Sie vermeiden, den Inhalt dieser Eigenschaft direkt festzulegen . Das Zusammenspiel zwischen Ansichten und Ebenen ergibt sich normalerweise." in der Ansicht, den Inhalt dieser Eigenschaft während eines nachfolgenden Updates zu ersetzen. ") Ich empfehle diesen Ansatz nicht besonders; Vorzeitiges Zeichnen beeinträchtigt die Leistung und die Zeichenqualität. Aber wenn Sie es aus irgendeinem Grund brauchen, dann contentsist, wie Sie es bekommen.
Rob Napier
@RobNapier Punkt mit drawRect:direktem Anruf genommen . Es war nicht notwendig, diese Technik zu demonstrieren, und wurde behoben.
Slipp D. Thompson
@RobNapier Was die contentsTechnik betrifft, die Sie vorschlagen ... klingt verlockend. Ich habe anfangs so etwas versucht, konnte es aber nicht zum Laufen bringen und fand, dass die obige Lösung viel weniger Code ist, ohne Grund, nicht genauso performant zu sein. Wenn Sie jedoch eine funktionierende Lösung für diesen contentsAnsatz haben, würde ich sie gerne lesen (es gibt keinen Grund, warum Sie nicht zwei Antworten auf diese Frage haben können, oder?)
Slipp D. Thompson
@RobNapier Hinweis: Dies hat nichts mit CALayer zu tun display. Es ist nur eine benutzerdefinierte öffentliche Methode in einer UIView-Unterklasse, genau wie in GLKView (eine andere UIView-Unterklasse und die einzige von Apple geschriebene, von der ich weiß, dass sie DRAW NOW! -Funktionalität erfordert ).
Slipp D. Thompson
5

Ich hatte das gleiche Problem und alle Lösungen von SO oder Google funktionierten nicht für mich. Normalerweise setNeedsDisplayfunktioniert es, aber wenn es nicht funktioniert ...
Ich habe versucht, setNeedsDisplaydie Ansicht auf jede mögliche Weise von allen möglichen Threads und Dingen aufzurufen - immer noch kein Erfolg. Wir wissen, wie Rob sagte, dass

"Dies muss im nächsten Ziehzyklus gezeichnet werden."

Aber aus irgendeinem Grund würde es diesmal nicht zeichnen. Und die einzige Lösung, die ich gefunden habe, besteht darin, sie nach einiger Zeit manuell aufzurufen, damit alles, was die Auslosung blockiert, wie folgt vergeht:

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

Dies ist eine gute Lösung, wenn Sie die Ansicht nicht häufig zum Neuzeichnen benötigen. Andernfalls gibt es normalerweise keine Probleme, wenn Sie nur telefonieren (Action) setNeedsDisplay.

Ich hoffe, es wird jemandem helfen, der dort verloren ist, so wie ich es war.

Dreamzor
quelle
0

Nun, ich weiß, dass dies eine große Änderung sein könnte oder sogar nicht für Ihr Projekt geeignet ist, aber haben Sie darüber nachgedacht , den Push erst durchzuführen, wenn Sie bereits über die Daten verfügen ? Auf diese Weise müssen Sie die Ansicht nur einmal zeichnen, und die Benutzererfahrung wird ebenfalls besser - der Push wird bereits geladen verschoben.

Die Art und Weise, wie Sie dies tun, besteht darin, dass UITableView didSelectRowAtIndexPathSie asynchron nach den Daten fragen. Sobald Sie die Antwort erhalten haben, führen Sie die Übergabe manuell durch und übergeben die Daten an Ihren viewController in prepareForSegue. In der Zwischenzeit möchten Sie möglicherweise einen Aktivitätsindikator anzeigen. Überprüfen Sie zum einfachen Laden des Indikators https://github.com/jdg/MBProgressHUD

Miki
quelle