Sorge für flüssiges Zeichnen von Rechtecken, auch wenn sich Formen ansammeln, indem du die aktive Ziehvorschau von den festgeschriebenen Canvas-Formen trennst. In einer Mac-Zeichen-App sollte eine Gestenfläche die Eingabe übernehmen, temporäre Geometrie lokal halten und die Formensammlung erst ergänzen, wenn das Ziehen erfolgreich abgeschlossen ist.
Diese Anleitung richtet sich an Entwickler, die in ihrer eigenen macOS-App ein einfaches Rechteckwerkzeug zum Anklicken und Ziehen erstellen. Alle Beispielformen liegen im Arbeitsspeicher. Ziel ist, zu verhindern, dass Zeigerbewegungen wiederholt das gesamte Zeichenmodell verändern, und anschließend mithilfe von Profiling die verbleibenden Rendering-Kosten zu ermitteln.
Warum wird das Zeichnen langsamer, wenn sich festgeschriebene Formen ansammeln?
Unterscheide zunächst drei Vorgänge: Eingaben empfangen, Zustand aktualisieren und Geometrie rendern. Während eines Ziehvorgangs kann sich die temporäre Geometrie häufig ändern. Wenn diese Änderungen auch die festgeschriebene Sammlung ersetzen, jeden Pfad neu aufbauen oder eine umfangreiche View-Hierarchie ungültig machen, kann der Aufwand für jede Bewegung mit der Zeichnung anwachsen.
Apple erklärt, dass SwiftUI Abhängigkeiten verfolgt und Views aktualisiert, wenn sich diese Abhängigkeiten ändern. Sowohl langsame Aktualisierungen einzelner Views als auch zahlreiche unnötige Aktualisierungen können dazu führen, dass Frame-Fristen verpasst werden. Den häufig wechselnden Vorschauzustand vom festgeschriebenen Inhalt zu trennen, ist eine architektonische Anwendung dieses dokumentierten Modells. SwiftUI-Performance mit Instruments optimieren
Canvas ermöglicht unmittelbares Zeichnen über einen Grafikkontext. Apple nennt es als mögliche Leistungsverbesserung für komplexe dynamische Zeichnungen, weist jedoch darauf hin, dass es keine Interaktion oder Barrierefreiheit für einzelne gezeichnete Elemente bietet. Verknüpfe die Eingabeverarbeitung mit einer gezielt dafür vorgesehenen Zeichenfläche. Falls die App später einzeln bedienbare Formen benötigt, implementiere eine separate Auswahl- und Barrierefreiheitsunterstützung. Canvas
Die Zuständigkeiten sind also klar festgelegt: Die Eingabefläche verwaltet die Geste, der temporäre Zustand die Vorschau und die übergeordnete View die festgeschriebenen Rechtecke. Durch die Aufteilung dieser Aufgaben werden vermeidbare Abhängigkeiten reduziert. Sie garantiert nicht, dass das System die festgeschriebene Zeichenfläche nie neu zeichnet.
Vorschau und festgeschriebene Zeichnung voneinander trennen
Lege diese Zuständigkeiten fest, bevor du Rendering-Optimierungen auswählst:
| Zuständigkeit | Eigentümer | Wann sie sich ändert |
|---|---|---|
| Aktuelles Ziehrechteck | Gestenfläche | Während des aktiven Ziehvorgangs |
| Sammlung festgeschriebener Rechtecke | Übergeordnete Zeichen-View | Einmal pro erfolgreich abgeschlossenem Ziehvorgang |
| Zeichnen festgeschriebener Rechtecke | Hintergrund-Canvas | Wenn dessen Eingaben eine Aktualisierung erfordern |
| Zeigererkennung | Gestenfläche | Während der gesamten Gestenfolge |
Für temporäre Geometrie ist @GestureState nützlich, da SwiftUI den Zustand zurücksetzt, sobald die Geste inaktiv wird. Schreibe das Ergebnis über onEnded fest, dessen Callback den endgültigen Gestenwert erhält. Apple gibt an, dass dieser Callback nur ausgeführt wird, wenn die Geste erfolgreich endet; der Gestenzustand kümmert sich unabhängig davon, wie sie endet, um die Bereinigung. GestureState, onEnded(_:)
Für Rechtecke werden zur Berechnung der endgültigen Geometrie nur die Startposition und die aktuelle Position benötigt. Es ist nicht nötig, jede Zwischenposition des Zeigers zu speichern. Apple dokumentiert diese Positionen getrennt von vorhergesagten Endpunkten. Verwende beim Festschreiben des Rechtecks die tatsächliche Endposition. DragGesture.Value
Ein vollständiges Rechteckwerkzeug im Arbeitsspeicher erstellen
Die folgenden beiden Codeblöcke bilden zusammen eine vollständige Swift-Quelldatei. Ersetze in einem SwiftUI-App-Projekt für macOS 12 oder neuer die generierte App-Einstiegsdatei durch beide Blöcke und entferne alle weiteren App-Einstiegsdeklarationen. Canvas ist ab macOS 12 verfügbar. Dieses Beispiel wurde geprüft, konnte hier aber nicht kompiliert werden, da in der Umgebung keine Swift-Toolchain vorhanden ist. Canvas
Der erste Block definiert die App, die Sammlung festgeschriebener Formen und deren Renderer. Die übergeordnete View erhält nur fertige Rechtecke; sie liest den aktiven Ziehzustand nicht aus.
import SwiftUI
@main
struct RectangleDrawingApp: App {
var body: some Scene {
WindowGroup {
DrawingBoard()
}
}
}
struct DrawingBoard: View {
@State private var rectangles: [CGRect] = []
var body: some View {
VStack(spacing: 8) {
Text("Drag to draw a rectangle")
Text("Committed rectangles: \(rectangles.count)")
ZStack {
Color.white
CommittedRectangles(rectangles: rectangles)
.allowsHitTesting(false)
RectangleGestureSurface { rectangle in
rectangles.append(rectangle)
}
}
.clipped()
.frame(minWidth: 480, minHeight: 320)
}
.padding()
}
}
struct CommittedRectangles: View {
let rectangles: [CGRect]
var body: some View {
Canvas { context, _ in
for rectangle in rectangles {
context.stroke(
Path(rectangle),
with: .color(.blue),
lineWidth: 2
)
}
}
}
}
Füge den folgenden Block in dieselbe Datei ein. Die transparente Fläche deckt den Zeichenbereich ab, legt eine explizite rechteckige Form für die Treffertests fest und verwaltet als einzige die Zeichengeste. Apple dokumentiert, dass contentShape die Form für Treffertests definiert und allowsHitTesting(false) eine View von Treffertests ausschließt. contentShape(_:eoFill:), allowsHitTesting(_:)
struct RectangleGestureSurface: View {
let commit: (CGRect) -> Void
@GestureState private var preview: CGRect? = nil
var body: some View {
ZStack {
Color.clear
Canvas { context, _ in
if let rectangle = preview {
context.stroke(
Path(rectangle),
with: .color(.orange),
lineWidth: 2
)
}
}
.allowsHitTesting(false)
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($preview) { value, state, _ in
state = Self.rectangle(
from: value.startLocation,
to: value.location
)
}
.onEnded { value in
let rectangle = Self.rectangle(
from: value.startLocation,
to: value.location
)
commit(rectangle)
}
)
}
private static func rectangle(
from start: CGPoint,
to end: CGPoint
) -> CGRect {
CGRect(
x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(end.x - start.x),
height: abs(end.y - start.y)
)
}
}
minimumDistance: 0 hebt die für die Erkennung erforderliche Mindestbewegung auf. Der lokale Koordinatenraum sorgt dafür, dass die Gestenpositionen mit der gleich großen Vorschau-Canvas übereinstimmen. Dies sind dokumentierte Konfigurationsoptionen für Gesten und keine Garantie für die Ereignisfrequenz oder die Anzeigelatenz. DragGesture
Die Rechteck-Hilfsfunktion normalisiert Ziehvorgänge in alle vier Richtungen. Als Beispiel ergibt das Ziehen von (90, 80) nach (30, 20) einen Ursprung von (30, 20) und eine Größe von (60, 60). Für Vorschau und Festschreiben wird dieselbe Berechnung verwendet, sodass sich die Geometrieformel beim Loslassen des Zeigers nicht ändert.
Dieses minimale Werkzeug schreibt auch Rechtecke mit einer Breite oder Höhe von null fest. Ein Klick kann deshalb den Zähler erhöhen, ohne ein sichtbares Rechteck zu erzeugen. Wenn das Produkt eine Mindestgröße erfordert, füge diese Regel explizit beim Festschreiben hinzu und informiere den Benutzer entsprechend. Das Beispiel behält jede erfolgreich abgeschlossene Geste bei, statt kleine Ergebnisse stillschweigend herauszufiltern.
Der sichtbare Bereich wird beschnitten, die gespeicherte Geometrie bleibt jedoch unverändert. Ein Rechteck, das über das Zeichenbrett hinausragt, kann daher Koordinaten außerhalb des Bretts enthalten. Entscheide, ob eine spätere Größenänderung diese Geometrie sichtbar machen soll, bevor du Koordinaten begrenzt.
Verbleibende Kosten ermitteln, bevor du einen Cache hinzufügst
Das Beispiel entfernt Änderungen am festgeschriebenen Modell aus dem Pfad für Ziehaktualisierungen. Der Renderer für festgeschriebene Formen durchläuft die Sammlung weiterhin jedes Mal, wenn er ausgeführt wird. Unterscheide zwischen verringerter Zustandsweitergabe und verringertem Rendering-Aufwand: Eine separate View ist kein dokumentierter dauerhafter Bild-Cache.
Finde mithilfe eines kontrollierten Vergleichs heraus, welcher Aufwand bestehen bleibt. Wähle beispielsweise In-Memory-Sammlungen mit 0, 500 und 5.000 Rechtecken. Behalte Rechteckabmessungen, Fenstergröße, Anzeige-Skalierung und Build-Konfiguration bei. Diese Anzahlen sind Testwerte und keine Kapazitätsgrenzen oder Messergebnisse.
Erfasse drei Zeitabschnitte getrennt: Ziehen, während die Sammlung unverändert bleibt; Loslassen zum Festschreiben; und Ändern der Fenstergröße. Eine durchgehend langsame Ziehbewegung deutet auf andere Arbeit hin als eine Pause, die nur beim Festschreiben auftritt. Das Verhalten bei Größenänderungen kann den Neuaufbau von Geometrie oder Caches aufdecken, der beim normalen Zeichnen nicht auftritt.
Apples Profiling-Sitzung zu SwiftUI beschreibt neben dem Time Profiler die Werkzeuge Long View Body Updates, Long Representable Updates und den Cause-&-Effect-Graphen. Untersuche mit dem Abhängigkeitsgraphen, warum festgeschriebene Views während eines Ziehvorgangs aktualisiert werden. Verwende CPU-Samples, um aufwendigen Geometrieaufbau oder Rendering-Aufwand zu finden. Apple führte diesen Instrument-Workflow in Instruments 26 ein. SwiftUI-Performance mit Instruments optimieren
Bei umfassenderen Blockierungen beginnt Apples WWDC26-Sitzung zur Reaktionsfähigkeit mit dem Time Profiler und unterscheidet zwischen rechenintensiver Ausführung und Wartezeiten. Nutze diese Unterscheidung, bevor du Arbeit zwischen Executoren verschiebst oder den Rendering-Stack austauschst. Profilieren, beheben und überprüfen: App-Reaktionsfähigkeit mit Instruments verbessern
Die folgende Entscheidungshilfe ist eine technische Schlussfolgerung aus diesen Profiling-Werkzeugen und dem oben beschriebenen Zuständigkeitsmodell:
| Beobachtetes Verhalten | Zuerst untersuchen | Mögliche Änderung |
|---|---|---|
| Der Zeichenstatus der übergeordneten View ändert sich während der Zeigerbewegung | Callback für Gestenaktualisierungen und gemeinsam genutzter Zustand | Den Vorschauzustand in der Gestenfläche halten |
| Das Zeichnen festgeschriebener Formen dominiert die Samples während der Bewegung | Wiederholtes Durchlaufen und Erzeugen von Pfaden | Geometrie vorberechnen oder einen Cache für festgeschriebene Inhalte einführen |
| Pausen beim Loslassen werden mit zunehmender Sammlung länger | Array-Änderung und vollständiger Neuaufbau der Zeichnung | Gespeicherte Momentaufnahmen reduzieren und nur betroffene abgeleitete Inhalte neu aufbauen |
| Rechteckzeichnen läuft flüssig, lange Freihandstriche sind langsam | Größe des aktiven Pfads und dessen Neuaufbau | Den wachsenden aktiven Strich separat profilieren |
| Pausen treten vor allem beim Ändern der Fenstergröße auf | Größenabhängiges Rendering und Cache-Invalidierung | Ein explizites Verhalten für Größenänderungen bei abgeleiteten Inhalten festlegen |
Ein Cache schafft eine weitere Zuständigkeitsgrenze: Die Rechteckgeometrie bleibt maßgeblich, während der Cache austauschbare abgeleitete Inhalte enthält. Berücksichtige Zeichnungsgröße, Anzeigeskalierung und relevante Stile bei der Invalidierungsstrategie. Bei einer Ergänzung der Zeichnung ist die inkrementelle Aktualisierung des Caches eine mögliche Optimierung. Bearbeitungen und Löschungen erfordern eine klare Strategie für den Neuaufbau oder die regionale Invalidierung. Überprüfe das Ergebnis mit denselben Eingabesammlungen, bevor du die zusätzliche Komplexität beibehältst.
Array-Kopien an der Festschreibungsgrenze prüfen
Swift-Arrays haben Wertsemantik und verwenden Copy-on-Write-Speicher. Kopien können denselben Speicher verwenden, bis eine Änderung unabhängigen Speicher erfordert; Arrays mit Klasseninstanzen teilen weiterhin Referenzen auf diese Instanzen. Weder sollte man beschreiben, dass das Übergeben eines Arrays an eine View noch das Anhängen an ein Array immer jedes Element kopiert. Array
Speichere für dieses Werkzeug bei jeder Zeigeraktualisierung keine neue vollständige Momentaufnahme der Zeichnung. Wenn das Profiling Kopiervorgänge beim Festschreiben aufzeigt, prüfe, welche Momentaufnahmen noch bestehen und wie das Zeichenmodell gemeinsam genutzt wird. Werttypen allein zur Vermeidung von Kopien durch gemeinsam veränderliche Objekte zu ersetzen, ändert die Zuständigkeitssemantik und erfordert eine eigene Entwurfsentscheidung.
Asynchrone Darstellung als messbare Option betrachten
Apple beschreibt rendersAsynchronously als Möglichkeit, der Canvas die asynchrone Darstellung ihres Inhalts für die übergeordnete View zu gestatten. Diese Beschreibung verspricht weder eine schnellere Gestenerkennung noch verschiebt sie den Gesten-Handler zu einem Hintergrund-Executor oder verhindert das Durchlaufen der Sammlung. Bewerte diese Option separat, nachdem Eingabe- und Zustandsgrenzen festgelegt sind. rendersAsynchronously
Wann sollte AppKit die Zeichenfläche verwalten?
Verwende eine benutzerdefinierte AppKit-Fläche, wenn die Aufgabe eine direkte Verarbeitung über Responder-Methoden oder eine explizite Invalidierung von Zeichenbereichen erfordert. Apple beschreibt NSView als Infrastruktur zum Zeichnen und Verarbeiten von Ereignissen und unterstützt das Ableiten eigener Klassen für benutzerdefiniertes Verhalten. Dies ist eine unterstützte Alternative mit eigenen Rendering-Aufgaben. NSView
Bei einer AppKit-Implementierung bleibt die Reihenfolge gleich: Initialisiere temporäre Geometrie in mouseDown(with:), aktualisiere sie in mouseDragged(with:) und schreibe sie in mouseUp(with:) fest. Wandle event.locationInWindow in die Koordinaten der empfangenden View um, bevor du die Geometrie berechnest. Apples archivierte Anleitung zu Mausereignissen dokumentiert diese Reihenfolge und Koordinatenumwandlung; die aktuelle NSView-Referenz dokumentiert die unterstützten Responder-Methoden. Mausereignisse verarbeiten, NSView
Fordere eine Anzeige an, indem du needsDisplay = true setzt. Apple gibt an, dass die View im nächsten Durchlauf der Ereignisschleife erneut angezeigt wird. Bei einem regionsbasierten Entwurf solltest du die Vereinigung der alten und neuen Vorschaugrenzen invalidieren und um die Strichbreite erweitern. Eine regionale Invalidierung verringert das Durchlaufen nur dann, wenn die Zeichenimplementierung auch unveränderte Inhalte nicht verarbeitet. needsDisplay, Mausereignisse verarbeiten
Bette die Fläche mit NSViewRepresentable ein: Erstelle sie in makeNSView, gleiche externen Zustand in updateNSView ab und verwende einen Coordinator, um Änderungen an SwiftUI zurückzumelden. Aktualisiere die Weiterleitung von Callbacks bei Aktualisierungen und setze einen aktiven Entwurf nicht zurück, nur weil SwiftUI einen weiteren Representable-Wert übergibt. Apple gibt außerdem an, dass SwiftUI das Layout der dargestellten View steuert. Überschreibe daher nicht direkt deren verwalteten Frame oder ihre Bounds. NSViewRepresentable
Lege eine einzige Zuständigkeit für die Eingabe beim Zeichnen fest. Wenn AppKit die Zeichenfolge verarbeitet, schreibe dieselbe Folge nicht zusätzlich über eine überlappende SwiftUI-Ziehgestenerkennung fest. Bei einer direkten NSView-Unterklasse weist Apple Implementierungen, die die genannten Mausereignisse verarbeiten, an, für diese behandelten Ereignisse nicht die Superklassenimplementierung aufzurufen. NSView
Halte die erste Verbesserung klein und leicht zu beobachten: lokaler Vorschauzustand, eine einzige Festschreibungsgrenze und ein Renderer für festgeschriebene Formen, dessen Eingaben sich während der Bewegung nicht ändern. Vergleiche anschließend in Instruments wachsende synthetische Zeichnungen. Der resultierende Trace zeigt, ob die nächste sinnvolle Änderung das Abhängigkeitsmanagement, die Zuständigkeit für die Sammlung, die Geometrievorbereitung oder das Rendering betrifft.
Quellen
Canvas — Apple Developer Documentation.
GestureState — Apple Developer Documentation.
onEnded(_:) — Apple Developer Documentation.
DragGesture — Apple Developer Documentation.
DragGesture.Value — Apple Developer Documentation.
contentShape(_:eoFill:) — Apple Developer Documentation.
allowsHitTesting(_:) — Apple Developer Documentation.
Array — Apple Developer Documentation.
rendersAsynchronously — Apple Developer Documentation.
SwiftUI-Performance mit Instruments optimieren — Apple Developer.
Profilieren, beheben und überprüfen: App-Reaktionsfähigkeit mit Instruments verbessern — Apple Developer.
NSView — Apple Developer Documentation.
needsDisplay — Apple Developer Documentation.
NSViewRepresentable — Apple Developer Documentation.
Mausereignisse verarbeiten — Apple Documentation Archive.