Håll snabb rektangelritning responsiv genom att skilja den aktiva förhandsvisningen under dragningen från de fastställda formerna på ritytan. I en ritapp för Mac låter du en enda gestyta hantera inmatningen, håller tillfällig geometri lokal och lägger till i formsamlingen först när dragningen avslutas utan fel.

Den här guiden riktar sig till utvecklare som bygger ett enkelt rektangelverktyg med klick och drag i en egen macOS-app. Alla exempelformer finns i minnet. Målet är att förhindra att pekarrörelser upprepade gånger ändrar hela ritmodellen och sedan använda profilering för att identifiera eventuell återstående renderingskostnad.

Varför blir ritningen långsammare när antalet fastställda former ökar?

Börja med att skilja mellan tre operationer: ta emot indata, uppdatera tillstånd och rendera geometri. En dragning kan ändra tillfällig geometri ofta. Om dessa ändringar också ersätter den fastställda samlingen, bygger om varje bana eller ogiltigförklarar en stor del av vyhierarkin kan arbetet för varje rörelse öka i takt med ritningen.

Apple förklarar att SwiftUI spårar beroenden och uppdaterar vyer när dessa beroenden ändras. Både långsamma enskilda vyuppdateringar och många onödiga uppdateringar kan göra att bildrutornas tidsgränser missas. Att skilja ofta ändrat tillstånd för förhandsvisningen från fastställt innehåll är en arkitektonisk tillämpning av den dokumenterade modellen. Optimera SwiftUI-prestanda med Instruments

Canvas möjliggör direkt renderad ritning genom en grafik­kontext. Apple beskriver det som en möjlig prestandaförbättring för komplexa dynamiska ritningar, men det ger inte interaktion eller hjälpmedel för varje ritad komponent. Koppla indatahantering till en avsiktligt vald rityta. Implementera separat stöd för markering och hjälpmedel om appen senare behöver former som kan hanteras var för sig. Canvas

Den användbara gränsdragningen är alltså tydlig: inmatningsytan hanterar gesten, det tillfälliga tillståndet hanterar förhandsvisningen och den överordnade vyn hanterar fastställda rektanglar. Genom att dela upp ansvaret minskar onödiga beroenden. Det garanterar inte att systemet aldrig ritar om den fastställda ritytan.

Skilj förhandsvisningen från den fastställda ritningen

Använd följande plan för ägarskap innan du väljer renderingsoptimeringar:

Ansvar Ägare När det ändras
Aktuell dragrektangel Gestyta Under den aktiva dragningen
Samling med fastställda rektanglar Överordnad ritvy En gång per lyckad dragning
Rita fastställda rektanglar Bakgrundsduk När indata kräver en uppdatering
Pekarigenkänning Gestyta Under hela gestsekvensen

För tillfällig geometri är @GestureState användbart eftersom SwiftUI återställer det när gesten blir inaktiv. Fastställ formen via onEnded, vars återanrop tar emot det slutliga gestvärdet. Apple anger att detta återanrop körs endast när gesten avslutas utan fel. Gesttillståndet hanterar rensningen oavsett hur gesten avslutas. GestureState, onEnded(_:)

För rektanglar kräver den slutliga geometrin bara startpositionen och den aktuella positionen. Du behöver inte spara varje mellanliggande pekarposition. Apple dokumenterar dessa positioner separat från förutsagda slutpunkter; använd den faktiska slutpositionen när rektangeln fastställs. DragGesture.Value

Bygg ett komplett rektangelverktyg som lagrar allt i minnet

Följande två kodblock utgör en komplett Swift-källfil. I ett SwiftUI-app-projekt för macOS med målversion macOS 12 eller senare ersätter du den genererade appens startfil med båda blocken och tar bort eventuella andra deklarationer av appens startpunkt. Canvas finns tillgängligt från macOS 12. Exemplet har granskats men kunde inte kompileras här eftersom miljön saknar Swift-verktygskedja. Canvas

Det första blocket definierar appen, samlingen med fastställda former och dess renderare. Den överordnade vyn får bara färdiga rektanglar; den läser inte tillståndet för den aktiva dragningen.

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

Lägg till följande block i samma fil. Den genomskinliga ytan täcker ritområdet, ger träfftestningen en uttrycklig rektangulär form och hanterar den enda ritgesten. Apple dokumenterar contentShape som det som definierar formen för träfftestning och allowsHitTesting(false) som det som utesluter en vy från träfftestning. 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 tar bort minimikravet på förflyttning för igenkänning. Det lokala koordinatsystemet håller gestens positioner i linje med förhandsvisningsduken, som har samma storlek. Det här är dokumenterade konfigurationsalternativ för gester, inte en garanti för händelsefrekvens eller bildskärmslatens. DragGesture

Rektangelhjälpfunktionen normaliserar alla fyra dragriktningar. Som ett illustrativt exempel ger en dragning från (90, 80) till (30, 20) origo (30, 20) och storleken (60, 60). Förhandsvisningen och fastställningen använder samma beräkning, så geometriformeln ändras inte när du släpper pekaren.

Det här minimala verktyget fastställer även rektanglar med bredd eller höjd noll. Ett klick kan därför öka antalet utan att ge en synlig rektangel. Om produkten kräver en minsta storlek lägger du uttryckligen till den regeln när formen fastställs och ger användaren lämplig återkoppling. Exemplet behåller varje gest som avslutas utan fel i stället för att tyst filtrera bort små resultat.

Det synliga området beskärs, medan lagrad geometri förblir oförändrad. En rektangel som sträcker sig utanför ritbordet kan därför innehålla koordinater utanför det. Bestäm om en framtida storleksändring ska visa den geometrin innan du lägger till koordinatbegränsning.

Diagnostisera återstående kostnad innan du lägger till en cache

Exemplet tar bort ändringar i den fastställda modellen från uppdateringsvägen under dragningen. Den fastställda renderaren går fortfarande igenom samlingen varje gång renderaren körs. Skilj mellan minskad spridning av tillståndsändringar och minskat renderingsarbete: en separat vy är inte en dokumenterad beständig bildcache.

Gör en kontrollerad jämförelse för att hitta den kvarvarande kostnaden. Välj illustrativa antal former i minnet, till exempel 0, 500 och 5 000 rektanglar. Håll rektanglarnas mått, fönsterstorlek, bildskärmsskala och byggkonfiguration konstanta. Dessa antal är testindata, inte kapacitetsgränser eller uppmätta resultat.

Mät tre tidsintervall separat: dragning medan samlingen förblir oförändrad, släpp för att fastställa formen och ändring av fönstrets storlek. Om dragningen blir långsammare tyder det på annat arbete än en paus som bara uppstår när formen fastställs. Beteendet vid storleksändring kan avslöja geometri- eller cacheombyggnad som inte sker vid vanlig ritning.

Apples profileringssession om SwiftUI beskriver verktygen Long View Body Updates, Long Representable Updates och Cause & Effect Graph samt Time Profiler. Använd beroendegrafen för att undersöka varför fastställda vyer uppdateras under en dragning. Använd CPU-prover för att hitta kostsam geometriskonstruktion eller renderingsarbete. Apple introducerade det här arbetsflödet för instrument i Instruments 26. Optimera SwiftUI-prestanda med Instruments

Vid bredare avbrott börjar Apples session WWDC26 om responsivitet med Time Profiler och skiljer mellan aktivt arbete och tid som går åt till väntan. Gör den åtskillnaden innan du flyttar arbete mellan exekverare eller byter ut renderingsstacken. Profilera, åtgärda och verifiera: förbättra appens responsivitet med Instruments

Följande beslutsstöd är en teknisk slutsats utifrån dessa profileringsverktyg och ägarskapsdesignen ovan:

Observerat beteende Undersök först Möjlig ändring
Den överordnade ritningens tillstånd ändras när pekaren flyttas Gestens uppdateringsåteranrop och delat tillstånd Behåll tillståndet för förhandsvisningen i gestytan
Den fastställda ritningen dominerar proverna under rörelse Upprepad genomgång och skapande av banor Förberäkna geometrin eller inför en cache för fastställt innehåll
Pauserna vid släpp blir längre när samlingen växer Arrayändring och fullständig omkonstruktion av ritningen Minska antalet sparade ögonblicksbilder och bygg bara om påverkat härlett innehåll
Rektangelritningen är responsiv, men långa frihandsdragningar är långsamma Den aktiva banans storlek och rekonstruktion Profilera den växande aktiva banan separat
Pauser uppstår främst vid storleksändring Storleksberoende rendering och ogiltigförklaring av cache Definiera uttryckligt hur härlett innehåll ska hanteras vid storleksändring

En cache lägger till ytterligare en ägarskapsgräns: rektangelgeometrin förblir den auktoritativa datan och cachen är utbytbart härlett innehåll. Ta med ritningens storlek, bildskärmsskalan och relevant formatering i planen för ogiltigförklaring. För en ritning där innehåll bara läggs till är inkrementella cacheuppdateringar en möjlig optimering. Ändringar och borttagningar kräver en tydlig strategi för ombyggnad eller partiell ogiltigförklaring. Verifiera resultatet med samma indataantal innan du behåller den extra komplexiteten.

Kontrollera arraykopiering vid gränsen där former fastställs

Swift-arrayer har värdesemantik och använder kopiering vid skrivning. Kopior kan dela lagring tills en ändring kräver separat lagring. Arrayer som innehåller klassinstanser delar fortfarande referenser till dessa instanser. Varken att skicka en array till en vy eller att lägga till i en array bör beskrivas som att alltid kopiera varje element. Array

För det här verktyget bör du undvika att lagra en ny fullständig ögonblicksbild av ritningen vid varje pekaruppdatering. Om profileringen visar kopiering när formen fastställs undersöker du vilka ögonblicksbilder som fortfarande lever och hur ritmodellen delas. Att enbart ersätta värdetyper med delade muterbara objekt för att undvika kopiering ändrar ägarskapssemantiken och kräver ett separat designbeslut.

Behandla asynkron visning som ett alternativ som måste mätas

Apple definierar rendersAsynchronously som att det låter duken visa sitt innehåll asynkront för den överordnade vyn. Den beskrivningen utlovar inte snabbare gesthantering, flyttar inte gestbehandlaren till en bakgrundsexekverare och eliminerar inte genomgång av samlingen. Utvärdera det separat när gränserna för indata och tillstånd har fastställts. rendersAsynchronously

När bör AppKit hantera ritytan?

Använd en anpassad AppKit-yta när uppgiften kräver direkt hantering av respondermetoder eller uttrycklig ogiltigförklaring av ritområden. Apple dokumenterar NSView som infrastrukturen för ritning och händelsehantering och stöder underklassning för anpassat beteende. Det här är ett stödd alternativ med egna renderingsansvar. NSView

Bevara samma sekvens i en AppKit-implementation: initiera tillfällig geometri i mouseDown(with:), uppdatera den i mouseDragged(with:) och fastställ den i mouseUp(with:). Konvertera event.locationInWindow till den mottagande vyns koordinater innan du beräknar geometrin. Apples arkiverade guide för mushändelser dokumenterar sekvensen och koordinatkonverteringen. Den aktuella NSView-referensen dokumenterar de respondermetoder som stöds. Hantera mushändelser, NSView

Begär visning genom att ange needsDisplay = true. Apple anger att vyn då visas igen vid nästa körning av händelseslingan. I en områdesbaserad design ogiltigförklarar du föreningen av de gamla och nya gränserna för förhandsvisningen, utökad så att den omfattar linjebredden. Områdesbaserad ogiltigförklaring minskar bara genomgången om ritimplementationen också undviker att behandla opåverkat innehåll. needsDisplay, Hantera mushändelser

Bädda in ytan med NSViewRepresentable: skapa den i makeNSView, synkronisera externt tillstånd i updateNSView och använd en koordinator för att skicka ändringar tillbaka till SwiftUI. Uppdatera routningen av återanrop vid uppdateringar och undvik att återställa ett pågående utkast enbart för att SwiftUI tillhandahåller ett nytt representable-värde. Apple anger också att SwiftUI styr den representerade vyns layout. Skriv inte direkt över dess hanterade ram eller gränser. NSViewRepresentable

Välj en enda ägare för ritinmatningen. Om AppKit hanterar ritsekvensen ska du inte också fastställa den via en överlappande SwiftUI-draggest. För en direkt underklass av NSView instruerar Apple implementationer som hanterar de listade mushändelserna att inte anropa superklassimplementationen för de händelser de hanterar. NSView

Håll den första förbättringen liten och lätt att observera: lokalt tillstånd för förhandsvisningen, en enda gräns för att fastställa former och en fastställd renderare med oförändrad indata under rörelse. Jämför sedan syntetiska ritningar som växer med hjälp av Instruments. Spårningen visar om nästa användbara ändring hör hemma i beroendehantering, samlingsägarskap, geometriförberedelser eller rendering.

Källor