Utrzymuj płynne rysowanie prostokątów, oddzielając podgląd aktywnego przeciągania od zatwierdzonych kształtów na płótnie. W aplikacji do rysowania na Macu powierz obsługę wejścia jednemu obszarowi gestu, przechowuj tymczasową geometrię lokalnie i dodawaj kształt do kolekcji dopiero po pomyślnym zakończeniu przeciągania.

Ten poradnik jest przeznaczony dla programisty tworzącego proste narzędzie do rysowania prostokątów przez kliknięcie i przeciągnięcie we własnej aplikacji na macOS. Wszystkie przykładowe kształty są przechowywane w pamięci. Celem jest niedopuszczenie do wielokrotnego modyfikowania całego modelu rysunku przy ruchu wskaźnika, a następnie użycie profilowania do zidentyfikowania pozostałych kosztów renderowania.

Dlaczego rysowanie zwalnia, gdy przybywa zatwierdzonych kształtów?

Zacznij od rozróżnienia trzech operacji: odbierania danych wejściowych, aktualizowania stanu i renderowania geometrii. Przeciąganie może często zmieniać tymczasową geometrię. Jeśli te zmiany dodatkowo zastępują zatwierdzoną kolekcję, ponownie tworzą każdą ścieżkę albo unieważniają rozległą hierarchię widoków, koszt pracy związanej z każdym ruchem może rosnąć wraz z rysunkiem.

Apple wyjaśnia, że SwiftUI śledzi zależności i aktualizuje widoki, gdy te zależności się zmieniają. Zarówno powolne aktualizacje pojedynczych widoków, jak i liczne niepotrzebne aktualizacje mogą powodować niedotrzymanie terminów wyświetlania klatek. Oddzielenie często zmieniającego się stanu podglądu od zatwierdzonej zawartości to architektoniczne zastosowanie tego udokumentowanego modelu. Optymalizacja wydajności SwiftUI za pomocą Instruments

Canvas umożliwia rysowanie w trybie natychmiastowym za pomocą kontekstu graficznego. Apple wskazuje je jako możliwy sposób na poprawę wydajności złożonych, dynamicznych rysunków, ale nie zapewnia ono interakcji ani dostępności dla każdego narysowanego elementu. Obsługę wejścia przypisz do przemyślanego obszaru rysowania; jeśli aplikacja będzie później wymagać osobnej obsługi kształtów, dodaj dla nich oddzielną obsługę zaznaczania i dostępności. Canvas

Przydatny podział odpowiedzialności jest więc jasno określony: obszar wejściowy obsługuje gest, stan tymczasowy przechowuje podgląd, a widok nadrzędny przechowuje zatwierdzone prostokąty. Rozdzielenie tych obowiązków ogranicza zbędne zależności. Nie gwarantuje jednak, że system nigdy ponownie nie narysuje zatwierdzonego płótna.

Oddziel podgląd od zatwierdzonego rysunku

Zanim wybierzesz optymalizacje renderowania, zastosuj następujący podział odpowiedzialności:

Odpowiedzialność Właściciel Kiedy się zmienia
Aktualnie przeciągany prostokąt Obszar gestu Podczas aktywnego przeciągania
Kolekcja zatwierdzonych prostokątów Nadrzędny widok rysunku Raz po pomyślnym przeciągnięciu
Rysowanie zatwierdzonych prostokątów Płótno w tle Gdy jego dane wejściowe wymagają aktualizacji
Rozpoznawanie wskaźnika Obszar gestu Przez całą sekwencję gestu

Do przechowywania tymczasowej geometrii przydatne jest @GestureState, ponieważ SwiftUI resetuje ten stan, gdy gest przestaje być aktywny. Zatwierdź wynik przez onEnded, którego funkcja zwrotna otrzymuje końcową wartość gestu. Apple określa, że ta funkcja jest wywoływana tylko wtedy, gdy gest kończy się pomyślnie; stan gestu zajmuje się sprzątaniem niezależnie od sposobu zakończenia. GestureState, onEnded(_:)

Do określenia końcowej geometrii prostokąta wystarczą położenie początkowe i bieżące. Nie trzeba przechowywać każdego pośredniego położenia wskaźnika. Apple dokumentuje te położenia osobno od przewidywanych punktów końcowych; przy zatwierdzaniu prostokąta użyj rzeczywistego położenia końcowego. DragGesture.Value

Zbuduj kompletne narzędzie do rysowania prostokątów w pamięci

Poniższe dwa bloki kodu tworzą jeden kompletny plik źródłowy Swift. W projekcie aplikacji SwiftUI na macOS, którego celem jest macOS 12 lub nowszy, zastąp wygenerowany plik wejściowy aplikacji oboma blokami i usuń wszystkie pozostałe deklaracje punktu wejścia aplikacji. Canvas jest dostępne od macOS 12. Ten przykład został sprawdzony, ale nie można go było tutaj skompilować, ponieważ w środowisku nie ma toolchaina Swift. Canvas

Pierwszy blok definiuje aplikację, zatwierdzoną kolekcję i jej renderer. Widok nadrzędny otrzymuje wyłącznie ukończone prostokąty; nie odczytuje stanu aktywnego przeciągania.

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

Dodaj poniższy blok do tego samego pliku. Przezroczysty obszar pokrywa obszar rysowania, jawnie definiuje prostokątny kształt testowania trafień i obsługuje jedyny gest rysowania. Apple dokumentuje, że contentShape definiuje kształt używany do testowania trafień, a allowsHitTesting(false) wyklucza widok z testowania trafień. 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 usuwa wymaganie minimalnego przesunięcia potrzebnego do rozpoznania gestu. Lokalny układ współrzędnych utrzymuje zgodność położeń gestu z płótnem podglądu o takich samych wymiarach. Są to udokumentowane opcje konfiguracji gestu, a nie gwarancja dotycząca częstotliwości zdarzeń czy opóźnienia wyświetlania. DragGesture

Funkcja pomocnicza prostokąta normalizuje wszystkie cztery kierunki przeciągania. Przykładowo przeciągnięcie od (90, 80) do (30, 20) daje początek (30, 20) i rozmiar (60, 60). Podgląd i zatwierdzenie korzystają z tych samych obliczeń, więc zwolnienie wskaźnika nie zmienia wzoru wyznaczania geometrii.

To minimalne narzędzie zatwierdza także prostokąty o zerowej szerokości lub wysokości. Kliknięcie może więc zwiększyć licznik bez utworzenia widocznego prostokąta. Jeśli produkt wymaga minimalnego rozmiaru, dodaj tę regułę jawnie podczas zatwierdzania i odpowiednio poinformuj użytkownika. Przykład zachowuje każdy pomyślnie zakończony gest zamiast po cichu odrzucać małe wyniki.

Widoczny obszar jest przycinany, ale zapisana geometria pozostaje bez zmian. Prostokąt wychodzący poza obszar roboczy może więc zawierać współrzędne spoza niego. Zanim dodasz ograniczanie współrzędnych, zdecyduj, czy późniejsza zmiana rozmiaru ma ujawnić tę geometrię.

Zdiagnozuj pozostały koszt, zanim dodasz pamięć podręczną

Przykład usuwa modyfikowanie zatwierdzonego modelu ze ścieżki aktualizacji podczas przeciągania. Renderer zatwierdzonych kształtów nadal przechodzi jednak przez kolekcję za każdym razem, gdy zostaje uruchomiony. Rozróżnij ograniczenie propagacji stanu od ograniczenia pracy renderowania: osobny widok nie jest udokumentowaną trwałą pamięcią podręczną obrazu.

Wykonaj kontrolowane porównanie, aby ustalić, który koszt pozostał. Wybierz przykładowe liczebności danych przechowywanych w pamięci, takie jak 0, 500 i 5 000 prostokątów. Zachowaj te same wymiary prostokątów, rozmiar okna, skalę wyświetlania i konfigurację kompilacji. Te liczby to dane wejściowe testu, a nie limity pojemności ani zmierzone wyniki.

Zarejestruj osobno trzy przedziały: przeciąganie, podczas którego kolekcja pozostaje bez zmian, zwolnienie wskaźnika w celu zatwierdzenia oraz zmianę rozmiaru okna. Ciągłe spowolnienie podczas przeciągania sugeruje inny rodzaj pracy niż przycięcie występujące wyłącznie podczas zatwierdzania. Zachowanie podczas zmiany rozmiaru może ujawnić obliczanie geometrii lub odbudowywanie pamięci podręcznej, które nie występuje przy zwykłym rysowaniu.

Sesja Apple dotycząca profilowania SwiftUI opisuje narzędzia Long View Body Updates, Long Representable Updates i Cause & Effect Graph oraz Time Profiler. Użyj grafu zależności, by zbadać, dlaczego zatwierdzone widoki aktualizują się podczas przeciągania; próbki procesora pomogą znaleźć kosztowne tworzenie geometrii lub pracę renderowania. Apple wprowadziło ten przepływ pracy w Instruments 26. Optymalizacja wydajności SwiftUI za pomocą Instruments

W przypadku szerszych przycięć sesja WWDC26 Apple poświęcona responsywności zaczyna od Time Profilera i rozróżnia intensywne wykonywanie kodu od czasu spędzonego na oczekiwaniu. Uwzględnij to rozróżnienie, zanim przeniesiesz pracę między executorami lub zmienisz stos renderowania. Profiluj, naprawiaj i weryfikuj: popraw responsywność aplikacji za pomocą Instruments

Poniższa pomoc w podejmowaniu decyzji to wniosek inżynierski oparty na opisanych narzędziach profilowania i powyższym podziale odpowiedzialności:

Zaobserwowane zachowanie Co najpierw zbadać Możliwa zmiana
Stan rysunku w widoku nadrzędnym zmienia się podczas ruchu wskaźnika Funkcje zwrotne aktualizacji gestu i współdzielony stan Przechowuj stan podglądu w obszarze gestu
Podczas ruchu dominują próbki związane z rysowaniem zatwierdzonych kształtów Wielokrotne przechodzenie po kolekcji i tworzenie ścieżek Wstępnie oblicz geometrię albo wprowadź pamięć podręczną zatwierdzonej zawartości
Przycięcia po zwolnieniu wskaźnika nasilają się wraz ze wzrostem kolekcji Modyfikacja tablicy i ponowne tworzenie całego rysunku Ogranicz liczbę zachowywanych kopii stanu i odbudowuj tylko pochodną zawartość, której to dotyczy
Rysowanie prostokątów jest płynne, ale długie odręczne kreski zwalniają Rozmiar aktywnej ścieżki i jej ponowne tworzenie Osobno profiluj rozrastającą się aktywną kreskę
Przycięcia występują głównie podczas zmiany rozmiaru Renderowanie zależne od rozmiaru i unieważnianie pamięci podręcznej Określ jawne zachowanie pochodnej zawartości przy zmianie rozmiaru

Pamięć podręczna wprowadza kolejną granicę odpowiedzialności: geometria prostokątów pozostaje źródłem prawdy, a pamięć podręczna jest zastępowalną zawartością pochodną. Uwzględnij rozmiar rysunku, skalę wyświetlania i istotne style w projekcie unieważniania. Przy rysowaniu wyłącznie przez dodawanie kształtów aktualizowanie pamięci podręcznej przyrostowo jest możliwą optymalizacją; edycje i usuwanie wymagają jasnej strategii odbudowy lub regionalnego unieważniania. Zanim zachowasz dodatkową złożoność, zweryfikuj wynik na tych samych liczebnościach danych.

Sprawdź kopiowanie tablic na granicy zatwierdzania

Tablice w Swift mają semantykę wartości i stosują kopiowanie przy zapisie. Kopie mogą współdzielić pamięć, dopóki modyfikacja nie wymaga niezależnego miejsca; tablice zawierające instancje klas nadal współdzielą odwołania do tych instancji. Nie należy więc twierdzić, że samo przekazanie tablicy do widoku ani dopisanie elementu zawsze kopiuje każdy element. Array

W tym narzędziu unikaj zapisywania pełnej kopii rysunku przy każdej aktualizacji położenia wskaźnika. Jeśli profilowanie ujawni kopiowanie podczas zatwierdzania, sprawdź, które kopie stanu wciąż istnieją i jak współdzielony jest model rysunku. Zastąpienie typów wartościowych współdzielonymi, mutowalnymi obiektami wyłącznie po to, by uniknąć kopiowania, zmienia semantykę własności i wymaga osobnej decyzji projektowej.

Traktuj asynchroniczne wyświetlanie jako opcję do sprawdzenia pomiarami

Apple definiuje rendersAsynchronously jako umożliwiające asynchroniczne wyświetlanie zawartości płótna przez widok nadrzędny. Ten opis nie gwarantuje szybszego dostarczania gestów, nie przenosi obsługi gestu do executora działającego w tle ani nie eliminuje przechodzenia po kolekcji. Oceń tę opcję osobno po ustaleniu granic obsługi wejścia i stanu. rendersAsynchronously

Kiedy obsługę obszaru rysowania powierzyć AppKit?

Użyj własnego obszaru AppKit, gdy zadanie wymaga bezpośredniej obsługi metod respondera lub jawnego unieważniania regionów rysowania. Apple opisuje NSView jako infrastrukturę do rysowania i obsługi zdarzeń oraz dopuszcza tworzenie podklas na potrzeby własnego zachowania. To obsługiwana alternatywa z własnymi obowiązkami związanymi z renderowaniem. NSView

W implementacji AppKit zachowaj tę samą kolejność: inicjalizuj tymczasową geometrię w mouseDown(with:), aktualizuj ją w mouseDragged(with:) i zatwierdzaj w mouseUp(with:). Przed obliczeniem geometrii przekształć event.locationInWindow do układu współrzędnych widoku odbierającego zdarzenie. Zarchiwizowany przewodnik Apple dotyczący zdarzeń myszy opisuje tę kolejność i konwersję współrzędnych; aktualna dokumentacja NSView opisuje obsługiwane metody respondera. Obsługa zdarzeń myszy, NSView

Zażądaj wyświetlenia, ustawiając needsDisplay = true; Apple określa, że widok zostanie wtedy wyświetlony ponownie podczas następnego przebiegu pętli zdarzeń. W projekcie opartym na regionach unieważnij sumę obszarów starego i nowego podglądu, rozszerzoną o obrys. Unieważnianie regionów ogranicza przechodzenie po zawartości tylko wtedy, gdy implementacja rysowania pomija także elementy, których to nie dotyczy. needsDisplay, Obsługa zdarzeń myszy

Osadź obszar za pomocą NSViewRepresentable: utwórz go w makeNSView, uzgodnij stan zewnętrzny w updateNSView i użyj koordynatora do przekazywania zmian z powrotem do SwiftUI. Podczas aktualizacji odśwież trasowanie funkcji zwrotnych i nie resetuj aktywnego szkicu tylko dlatego, że SwiftUI przekazuje kolejną wartość reprezentowalną. Apple zaznacza też, że układem reprezentowanego widoku steruje SwiftUI; nie nadpisuj bezpośrednio zarządzanych przez nie wartości frame ani bounds. NSViewRepresentable

Wybierz jednego właściciela obsługi wejścia rysowania. Jeśli sekwencję rysowania obsługuje AppKit, nie zatwierdzaj jej jednocześnie przez nakładający się gest przeciągania SwiftUI. W przypadku bezpośredniej podklasy NSView Apple zaleca, by implementacje obsługujące wymienione zdarzenia myszy nie wywoływały implementacji klasy nadrzędnej dla obsługiwanych zdarzeń. NSView

Pierwsze usprawnienie powinno być niewielkie i łatwe do zaobserwowania: lokalny stan podglądu, pojedyncza granica zatwierdzania i renderer zatwierdzonej zawartości, którego dane wejściowe nie zmieniają się podczas ruchu wskaźnika. Następnie porównaj rosnące syntetyczne rysunki w Instruments. Uzyskany ślad pokaże, czy kolejna przydatna zmiana dotyczy zarządzania zależnościami, własności kolekcji, przygotowania geometrii czy renderowania.

Źródła