Po zakończeniu przeciągania na płótnie oblicz końcową geometrię elementu w układzie współrzędnych obiektu docelowego. Najpierw sprawdź pełne zawieranie, a następnie nakładanie się o dodatnim polu. Ten samouczek pokazuje, jak zastosować tę regułę do prostokątnych elementów we własnej aplikacji na Maca, i wyjaśnia jej odpowiednik w AppKit.

Określ, co uznajesz za nakładanie się i zawieranie

Rozważmy prostą aplikację na komputer z ruchomym niebieskim prostokątem i nieruchomym szarym obiektem docelowym. Zadaniem użytkownika jest ustalenie, czy upuszczony element mieści się w całości w obiekcie docelowym, częściowo na niego nachodzi, czy nie ma z nim wspólnego obszaru. Wszystkie dane w przykładzie są syntetyczne i przechowywane w pamięci.

Zawieranie jest kierunkowe: obiekt docelowy musi zawierać element. Apple dokumentuje, że CGRectContainsRect(target, item) zwraca wartość true, gdy suma prostokątów jest równa obiektowi docelowemu. Obejmuje to element dokładnie pokrywający się z obiektem docelowym. Sprawdzenie zawierania jako pierwszego pozwala uniknąć oznaczenia w pełni zawartego elementu jako częściowo nachodzącego. Dokumentacja CGRectContainsRect

W tym przykładzie nakładanie się wymaga wspólnego obszaru o dodatniej szerokości i dodatniej wysokości. Samo zetknięcie krawędzią lub narożnikiem jest klasyfikowane jako Brak nakładania powierzchni. Oblicz część wspólną i sprawdź jej wymiary, zamiast pozostawiać zachowanie przy zetknięciu niedookreślone. Apple dokumentuje, że CGRectIntersection zwraca wspólny prostokąt albo pusty prostokąt, gdy nie ma części wspólnej. Dokumentacja CGRectIntersection

Wynik Reguła użyta w tym samouczku Przykład
Zawarty Obiekt docelowy zawiera cały element, także gdy ich granice się pokrywają Element mieści się w obiekcie docelowym lub dokładnie go pokrywa
Nakładanie Zawieranie nie zachodzi, ale wspólny obszar ma dodatnią szerokość i wysokość Element nachodzi na krawędź obiektu docelowego
Brak nakładania powierzchni Część wspólna nie ma dodatniego pola Element jest oddzielony lub tylko styka się z krawędzią
Nieprawidłowa geometria Co najmniej jeden prostokąt ma niefinitywne współrzędne lub niedodatnie wymiary Element o zerowej szerokości

Są to reguły aplikacji wyprowadzone z operacji geometrycznych. Jeśli interfejs wymaga marginesu wewnątrz obiektu docelowego, określ go osobno i sprawdzaj względem mniejszego prostokąta akceptacji. Zadbaj o to, by widoczny obiekt docelowy i zasada akceptacji były zrozumiałe dla osoby przeciągającej element.

Używaj jednego układu współrzędnych i końcowego przesunięcia przeciągania

Rysowany prostokąt i klasyfikowany prostokąt muszą opisywać tę samą pozycję. W tym przykładzie zarówno element, jak i obiekt docelowy używają współrzędnych płótna. Płótno otrzymuje nazwany układ współrzędnych, a DragGesture elementu korzysta z tej samej nazwy. Aktualny modyfikator coordinateSpace(_:) firmy Apple obsługuje ten wzorzec w systemie macOS 14 i nowszych. Dokumentacja nazwanego układu współrzędnych

Zapisz zatwierdzony prostokąt elementu oddzielnie od tymczasowego przesunięcia przeciągania. Podczas gestu rysuj zatwierdzony prostokąt przesunięty o wektor przesunięcia. Po pomyślnym zakończeniu ponownie oblicz ten końcowy prostokąt na podstawie wartości z wywołania zwrotnego, sklasyfikuj go i zatwierdź jednorazowo.

Apple definiuje translation jako całkowite przesunięcie od początku przeciągania do bieżącego zdarzenia. Dlatego wielokrotne dodawanie całego przesunięcia do już zaktualizowanej pozycji powodowałoby nadmierne narastanie ruchu. Zastosuj je do niezmienionej pozycji zatwierdzonej. Dokumentacja przesunięcia przeciągania

Samo miejsce upuszczenia nie wystarczy, by stwierdzić, czy cały element mieści się w obiekcie docelowym. Użytkownik może chwycić prostokąt blisko jednego narożnika, przez co znaczna jego część będzie poza obiektem docelowym, nawet jeśli wskaźnik znajdzie się w jego środku. Zastosowanie przesunięcia zachowuje też pierwotne położenie chwytu, nie przenosząc środka elementu pod wskaźnik.

Utwórz kompletny przykład prostokąta w SwiftUI

W Xcode utwórz aplikację SwiftUI na macOS z minimalną wersją docelową macOS 14 lub nowszą. Zastąp wygenerowany punkt wejścia aplikacji dwoma poniższymi blokami, umieszczonymi kolejno w jednym pliku Swift. Usuń pierwotny punkt wejścia, tak aby cel zawierał tylko jedną deklarację @main. Oba bloki razem tworzą kompletny program.

Pierwszy blok definiuje klasyfikator geometrii. Jawnie odrzuca nieprawidłowe wymiary, zamiast po cichu przekształcać je w inny prostokąt. Wyliczenie w Swift rozróżnia cztery możliwe wyniki; jego surowe wartości typu String dostarczają wyświetlanych komunikatów. Dokumentacja wyliczeń w Swift

import SwiftUI
import CoreGraphics

enum Placement: String {
    case contained = "Contained"
    case overlap = "Overlap"
    case separate = "No area overlap"
    case invalid = "Invalid geometry"
}

func isUsable(_ rect: CGRect) -> Bool {
    let values = [rect.origin.x, rect.origin.y,
                  rect.width, rect.height,
                  rect.maxX, rect.maxY]
    return values.allSatisfy { $0.isFinite }
        && rect.width > 0 && rect.height > 0
}

func classify(item: CGRect, target: CGRect) -> Placement {
    guard isUsable(item), isUsable(target) else {
        return .invalid
    }
    if CGRectContainsRect(target, item) {
        return .contained
    }
    let shared = CGRectIntersection(item, target)
    if !shared.isNull && shared.width > 0 && shared.height > 0 {
        return .overlap
    }
    return .separate
}

Drugi blok zawiera aplikację i płótno. Dołącz gest do ruchomego prostokąta, zgodnie z opisem DragGesture w dokumentacji Apple. Stały rozmiar płótna sprawia, że geometria tego przykładu jest jednoznaczna. Dokumentacja DragGesture

@main
struct RectanglePlacementApp: App {
    var body: some Scene {
        WindowGroup {
            PlacementCanvas()
        }
    }
}

struct PlacementCanvas: View {
    private let target = CGRect(x: 200, y: 100,
                                width: 200, height: 160)
    @State private var item = CGRect(x: 40, y: 120,
                                     width: 80, height: 60)
    @State private var message = "Drag the blue item, then release."
    @GestureState private var translation: CGSize = .zero

    private var preview: CGRect {
        item.offsetBy(dx: translation.width,
                      dy: translation.height)
    }

    private var move: some Gesture {
        DragGesture(minimumDistance: 1,
                    coordinateSpace: .named("placementCanvas"))
            .updating($translation) { value, state, _ in
                state = value.translation
            }
            .onEnded { value in
                let finalItem = item.offsetBy(
                    dx: value.translation.width,
                    dy: value.translation.height
                )
                let result = classify(item: finalItem, target: target)
                message = result.rawValue
                if result != .invalid {
                    item = finalItem
                }
            }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(message)
            ZStack(alignment: .topLeading) {
                Color.white
                Rectangle()
                    .fill(Color.gray.opacity(0.25))
                    .frame(width: target.width, height: target.height)
                    .position(x: target.midX, y: target.midY)
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: item.width, height: item.height)
                    .position(x: preview.midX, y: preview.midY)
                    .gesture(move)
            }
            .frame(width: 640, height: 380)
            .coordinateSpace(.named("placementCanvas"))
            Button("Reset") {
                item = CGRect(x: 40, y: 120, width: 80, height: 60)
                message = "Drag the blue item, then release."
            }
        }
        .padding()
    }
}

Przeciągnij niebieski element na szary obiekt docelowy i upuść go. Komunikat zapisuje wynik ostatniego zakończonego umieszczenia. Elementy nachodzące częściowo i elementy umieszczone osobno pozostają tam, gdzie je upuszczono; zawieranie nie zmienia automatycznie relacji nadrzędności ani nie przyciąga elementu do innego kontenera. Przycisk Reset przywraca prostokąt początkowy.

W wywołaniu aktualizującym gest celowo nie ma trwałej aktualizacji pozycji. @GestureState przechowuje wyłącznie przesunięcie podglądu i resetuje się, gdy gest staje się nieaktywny. Apple określa, że onEnded jest wywoływane tylko po pomyślnym zakończeniu, a do wartości, które muszą się zresetować niezależnie od sposobu zakończenia gestu, zaleca stan gestu. W związku z tym przerwany gest może odrzucić podgląd, zachowując zatwierdzony prostokąt i poprzedni wynik. Dokumentacja GestureState, Dokumentacja onEnded

Ta implementacja traktuje wypełnione prostokąty jako geometrię. Dodanie dekoracyjnego cienia lub obramowania nie powiększa prostokąta akceptacji. Jeśli wprowadzisz zmianę rozmiaru, powiększenie lub obrót, zaktualizuj model geometrii i konwersję współrzędnych, zanim zaczniesz polegać na tym samym klasyfikatorze.

Sprawdź przypadki brzegowe przed dodaniem kolejnych elementów

Użyj poniższych syntetycznych przypadków z obiektem docelowym z przykładu: początek (200, 100), szerokość 200, wysokość 160. Współrzędne opisują prostokąty elementów w tym samym układzie współrzędnych płótna.

Początek elementu Rozmiar elementu Oczekiwany wynik Uzasadnienie
(220, 120) 80 × 60 Zawarty Każda krawędź znajduje się wewnątrz obiektu docelowego
(360, 120) 80 × 60 Nakładanie Element wychodzi poza prawą krawędź obiektu docelowego
(400, 120) 80 × 60 Brak nakładania powierzchni Stykają się tylko krawędzie pionowe
(401, 120) 80 × 60 Brak nakładania powierzchni Prostokąty rozdziela przerwa
(200, 100) 200 × 160 Zawarty Identyczne prostokąty spełniają warunek zawierania
(180, 80) 240 × 200 Nakładanie Element obejmuje obiekt docelowy, co odwraca kierunek zawierania

Obliczenia w tabeli sprawdzono za pomocą niezależnego, deterministycznego rachunku. Program w Swift został przeanalizowany, ale nie skompilowany w środowisku badawczym, które nie udostępniało kompilatora Swift. Zbuduj aplikację w docelowym projekcie na macOS i sprawdź pozycje upuszczenia przy każdej granicy.

W przypadku wielu elementów przechowywanych w pamięci zachowaj tożsamość elementu wybranego na początku przeciągania. Po zakończeniu porównaj jego końcową geometrię z kwalifikującymi się obiektami docelowymi i pomiń sam przeciągany element. Jeśli kwalifikuje się kilka obiektów docelowych, określ stabilną regułę wyboru, na przykład jawną kolejność. Sama klasyfikacja geometrii nie decyduje, który obiekt docelowy powinien być właścicielem elementu.

Zastosuj tę samą regułę geometrii w obszarze rysowania AppKit

W istniejącym niestandardowym widoku AppKit trzymaj klasyfikator oddzielnie od obsługi zdarzeń. mouseUp(with:) informuje o zwolnieniu lewego przycisku myszy; użyj go do finalizacji tylko przeciągania, za które odpowiada już twoje płótno. Niepowiązane zdarzenie mouse-up nie powinno uruchamiać umieszczania. Dokumentacja zdarzenia mouse-up w AppKit

Praktyczna sekwencja zdarzeń wygląda następująco:

  1. W mouseDown(with:) wybierz syntetyczny element, stosując istniejącą w płótnie regułę trafienia. Zapisz jego tożsamość, prostokąt początkowy i początkowe położenie wskaźnika.
  2. Przelicz położenie każdego istotnego zdarzenia myszy na współrzędne płótna. Dla widoku dołączonego do okna wyrażenie konwersji to convert(event.locationInWindow, from: nil).
  3. W mouseDragged(with:) wyznacz podgląd na podstawie zapisanego prostokąta i przesunięcia wskaźnika.
  4. W mouseUp(with:) ponownie oblicz przesunięcie na podstawie zdarzenia zwolnienia przycisku, sklasyfikuj końcowy prostokąt, zatwierdź go jednorazowo i wyczyść aktywne przeciąganie.

Apple dokumentuje, że locationInWindow oznacza położenie zdarzenia myszy w układzie współrzędnych okna. Przeciążenie NSView.convert(_:from:) służące do konwersji punktu przelicza współrzędne z układu okna, gdy jego argumentem view jest nil. Dzięki temu nie porównujesz punktu w układzie okna z prostokątem wyrażonym w lokalnych współrzędnych płótna. Dokumentacja położenia zdarzenia, Dokumentacja konwersji punktu

Obsługę zdarzeń pozostaw komponentowi, który już odpowiada za interakcję. Apple rozróżnia bezpośrednią podklasę NSView, która obsługuje zdarzenie, od podklasy, która musi przekazać zdarzenia nieobsłużone rodzicowi. Postępuj zgodnie z udokumentowanym zachowaniem łańcucha responderów; dodanie logiki umieszczania nie uzasadnia przechwytywania sekwencji innego widoku ani obsługi tego samego zwolnienia w dwóch niezależnych procedurach. Wskazówki Apple dotyczące obsługi zdarzeń w NSView

Pamiętaj, że prostokąty nie zawsze odpowiadają na pytanie o kształt

Przykład daje dokładny wynik dla wypełnionych prostokątów ustawionych równolegle do osi. W przypadku kół, wklęsłych obrysów, otworów lub obróconych kształtów prostokąty ograniczające pozwalają jedynie wstępnie ocenić geometrię. Ich wspólny obszar może obejmować przestrzeń, której nie wypełnia żaden z widocznych kształtów.

Sprawdzenie, czy punkt wskaźnika znajduje się wewnątrz ścieżki, również nie pozwala stwierdzić, czy inny kształt jest w niej w całości zawarty. AppKit udostępnia metodę NSBezierPath.contains(_:), która przyjmuje pojedynczy punkt w układzie współrzędnych ścieżki, a Apple zaznacza, że nie uwzględnia ona szerokości obrysu. Sprawdzenie środka elementu odpowiada więc tylko na pytanie, czy ten środek znajduje się wewnątrz ścieżki. Dokumentacja zawierania punktu w ścieżce NSBezierPath

Dla wypełnionych ścieżek w systemie macOS 14 i nowszych SwiftUI dokumentuje obsługiwane operacje logiczne: Path.intersection(_:eoFill:) tworzy wspólny wypełniony obszar, a Path.subtracting(_:eoFill:) tworzy część pierwszego wypełnionego obszaru, która znajduje się poza drugim. Operacje te umożliwiają badanie nakładania się kształtów na podstawie ścieżek oraz części elementu leżącej poza obiektem docelowym. Wyrównaj obie ścieżki w tym samym układzie współrzędnych i użyj reguły wypełnienia zgodnej ze sposobem renderowania. Dokumentacja przecięcia ścieżek, Dokumentacja odejmowania ścieżek

Te operacje dotyczą wypełnionych obszarów i zakładają, że otwarte podścieżki są zamknięte. Linia z obrysem lub reguła aplikacji dotycząca styku granic wymaga własnej definicji geometrii. Zachowaj klasyfikator prostokątny dla elementów prostokątnych; świadomie rozszerz warstwę geometrii, gdy płótno zacznie rysować inne kształty.

Wiarygodny wynik po upuszczeniu wynika z trzech konkretnych decyzji: zachowania geometrii początkowej przeciągania, obliczenia końcowej geometrii elementu we współrzędnych obiektu docelowego oraz zastosowania jawnej zasady zawierania i nakładania się. Gdy te decyzje są ustalone, interfejs może konsekwentnie wyświetlać wynik bez łączenia geometrii z trasowaniem zdarzeń ani własnością elementu.

Źródła