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