Przywracaj przyciemnione kształty, obrysy i etykiety przeciągania w wywołaniu zwrotnym końca sesji źródła przeciągania. Wywołanie zwrotne opuszczenia obszaru docelowego oznacza jedynie, że wskaźnik opuścił ten obszar. W aplikacji na Maca użyj interfejsu API sesji źródłowej SwiftUI, jeśli jest dostępny, albo powierz obsługę przeciągania źródłu AppKit.
Ten poradnik dotyczy jednego zadania: przywrócenia normalnego wyglądu syntetycznego kształtu po zakończeniu przeciągania przez użytkownika, także wtedy, gdy żaden obszar docelowy go nie zaakceptuje. Przykład przechowuje zawartość w pamięci i traktuje zaakceptowane upuszczenie jako kopiowanie, więc przywracanie informacji zwrotnej nigdy nie wymaga usunięcia ani przeniesienia źródła.
Dlaczego informacja zwrotna o przeciąganiu pozostaje aktywna poza obszarami docelowymi?
Typowa implementacja ustawia flagę isDragging po rozpoczęciu przeciągania i czyści ją w performDrop. Wiąże to czyszczenie ze zdarzeniem, które może obsłużyć tylko akceptujący obszar docelowy. Upuszczenie w pustym miejscu okna nie pozostawia żadnego obszaru docelowego odpowiedzialnego za zresetowanie źródła.
Apple definiuje DropDelegate jako interfejs widoku zmodyfikowanego tak, by akceptował upuszczane elementy. Jego metoda dropExited(info:) informuje, że zweryfikowane przeciąganie opuściło ten widok. Przeciąganie może być kontynuowane do innego obszaru docelowego, wrócić do pierwszego albo zakończyć się gdzie indziej. Dlatego opuszczenie obszaru docelowego nie pozwala stwierdzić, że sesja źródłowa się zakończyła. DropDelegate
To samo rozróżnienie dotyczy dropDestination(for:action:isTargeted:): isTargeted opisuje, czy wskaźnik znajduje się wewnątrz obszaru docelowego. Używaj go do sterowania obramowaniem lub tłem obszaru docelowego. Przyciemnieniem źródła lub etykietą przeciągania powinno sterować wywołanie zwrotne sesji źródłowej. dropDestination(for:action:isTargeted:)
Z udokumentowanych granic odpowiedzialności wynika przydatna zasada projektowa: przypisuj każdy stan wizualny do właściciela zdarzenia, który może niezawodnie zakończyć jego obsługę.
| Informacja zwrotna lub wynik | Właściciel | Punkt resetu lub zatwierdzenia |
|---|---|---|
| Przyciemniony kształt źródłowy | Źródło przeciągania | Koniec sesji źródłowej |
| Podświetlony obszar docelowy | Obszar docelowy | Opuszczenie obszaru przez wskaźnik; w przypadku lokalnego przeciągania także defensywne czyszczenie po zakończeniu sesji źródłowej |
| Zaakceptowany element syntetyczny | Obszar docelowy/model | Pomyślna walidacja upuszczenia |
| Tymczasowe eksportowane zasoby | Cykl życia transferu | Zakończenie transferu, jeśli ma zastosowanie |
Którego obsługiwanego podejścia powinna użyć aplikacja na Maca?
Aktualna dokumentacja Apple podaje, że onDragSessionUpdated(_:) jest dostępne w systemie macOS 26 i nowszych. Obserwuje aktualizacje najbliższej sesji przeciągania w hierarchii widoków podrzędnych. Umieść je bezpośrednio poza modyfikatorem przeciągania źródła, aby zakres był jasny. Użyj SDK, które faktycznie deklaruje tę metodę; dostępność w czasie wykonywania nie sprawi, że kod się skompiluje, jeśli brakuje deklaracji w SDK. onDragSessionUpdated(_:)
W dokumentacji występuje rozbieżność, którą warto obsłużyć w ograniczony sposób: przykład modyfikatora pokazuje fazę zakończenia z argumentami lokalizacji i operacji, natomiast osobna dokumentacja faz deklaruje ended(DropOperation). Poniższy przykład dopasowuje .ended bez pobierania powiązanych wartości, ponieważ przywrócenie wyglądu ich nie wymaga. Przed dodaniem zachowania zależnego od operacji sprawdź deklarację w zainstalowanym SDK. DragSession.Phase.ended(_:)
Jeśli docelowa wersja systemu lub SDK nie obsługuje wywołania zwrotnego SwiftUI, źródło przeciągania AppKit zapewnia udokumentowany punkt zakończenia. Apple wyraźnie stwierdza, że źródło otrzymuje draggingSession(_:endedAt:operation:) po zakończeniu lub anulowaniu przeciągania. Własne źródło musi samo zainicjować tę sesję. beginDraggingSession(with:event:source:)
Jeśli chcesz jedynie zmieniać położenie kształtu na pojedynczym obszarze roboczym, rozważ lokalny DragGesture zamiast systemowej sesji przeciągania i upuszczania. @GestureState resetuje się, gdy gest staje się nieaktywny, więc nadaje się do tymczasowej informacji zwrotnej związanej z tym gestem. Traktuj ten wybór oddzielnie od przeciągania systemowego, które udostępnia obszarom docelowym przenoszoną zawartość. GestureState
Implementacja czyszczenia po stronie źródła w SwiftUI
Utwórz aplikację SwiftUI na macOS, używając SDK udostępniającego onDragSessionUpdated(_:), ustaw docelową wersję systemu na macOS 26 lub nowszą i zastąp wygenerowane deklaracje aplikacji oraz widoku zawartości poniższym kompletnym programem. Udostępnia on jeden syntetyczny ciąg znaków shape.circle i jeden obszar przyjmujący upuszczane elementy.
Źródło używa draggable(_:), aby udostępnić przenoszoną zawartość. Obszar docelowy akceptuje dokładnie jeden pasujący ciąg znaków i zwraca false dla innych danych wejściowych. Apple opisuje wynik logiczny obszaru docelowego jako informację, czy upuszczenie się powiodło. draggable(_:), dropDestination(for:action:isTargeted:)
import SwiftUI
@main
struct DragFeedbackDemoApp: App {
var body: some Scene {
WindowGroup {
DragFeedbackDemo()
}
}
}
struct DragFeedbackDemo: View {
private let payload = "shape.circle"
@State private var isDragging = false
@State private var isTargeted = false
@State private var acceptedCopies = 0
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle into the receiving area")
Circle()
.fill(Color.blue)
.frame(width: 72, height: 72)
.opacity(isDragging ? 0.4 : 1)
.draggable(payload)
.onDragSessionUpdated { session in
switch session.phase {
case .initial, .active:
isDragging = true
case .ended, .dataTransferCompleted:
restoreFeedback()
default:
break
}
}
Text(isDragging ? "Dragging" : "Ready")
RoundedRectangle(cornerRadius: 12)
.fill(isTargeted
? Color.blue.opacity(0.2)
: Color.gray.opacity(0.12))
.overlay {
Text("Receiving area")
}
.frame(width: 280, height: 120)
.dropDestination(for: String.self) { items, _ in
guard items.count == 1,
items.first == payload else {
return false
}
acceptedCopies += 1
return true
} isTargeted: { targeted in
isTargeted = targeted
}
Text("Accepted copies: \(acceptedCopies)")
}
.padding(32)
.frame(minWidth: 380, minHeight: 400)
}
private func restoreFeedback() {
isDragging = false
isTargeted = false
}
}
Te dwie flagi pełnią różne funkcje. isDragging zmienia krycie źródła. isTargeted zmienia wypełnienie obszaru przyjmującego upuszczane elementy. Wchodzenie na ten obszar i opuszczanie go nie może więc przypadkowo przywrócić wyglądu źródła, gdy przeciąganie jest nadal aktywne.
Dokumentacja faz wskazuje, że initial oznacza sesję, która ma się rozpocząć, active — ruch, a ended — zakończenie. Przykład przywraca informację zwrotną w ended; ten sam reset można też bezpiecznie powtórzyć w dataTransferCompleted. Przywrócenie wyglądu po nieudanym przeciągnięciu nigdy nie zależy wyłącznie od zakończenia transferu. DragSession.Phase
Apple opisuje dataTransferCompleted jako moment, w którym przeniesione elementy zostały przekazane i można posprzątać tymczasowe elementy. Ten przykład nie korzysta z tymczasowych zasobów eksportu. W większej implementacji oddziel czyszczenie wyglądu od czyszczenia zasobów, aby przywrócenie krycia nie usuwało czegoś, czego transfer nadal potrzebuje. DragSession.Phase.dataTransferCompleted
Okrąg pozostaje w hierarchii przez całą sesję. Zaakceptowane upuszczenie zwiększa licznik obszaru docelowego; niezaakceptowane nie zmienia zawartości. Dzięki temu przykład pozwala diagnozować informację zwrotną niezależnie od zmiany kolejności lub semantyki przenoszenia. Kod sprawdzono pod kątem pobranych deklaracji; nie skompilowano go ani nie uruchomiono, ponieważ dostępne środowisko to Linux bez SwiftUI.
Jak obejście AppKit powinno zachować odpowiedzialność za zdarzenia?
W przypadku starszej obsługiwanej wersji macOS zastąp implementację źródła przeciągania widokiem NSView osadzonym przez NSViewRepresentable. Reprezentowalny widok tworzy i aktualizuje widok natywny oraz przekazuje jego wywołania zwrotne do stanu SwiftUI. SwiftUI steruje układem reprezentowanego widoku, dlatego używaj modyfikatorów rozmiaru SwiftUI zamiast samodzielnie przypisywać ramkę reprezentowanemu widokowi. NSViewRepresentable
Zaimplementuj obejście jako jedną sekwencję obsługiwaną przez źródło:
- W metodzie
mouseDown(with:)natywnego widoku źródłowego zachowaj rzeczywiste zdarzenie naciśnięcia przycisku myszy dostarczone do tego widoku. - W
mouseDragged(with:)zastosuj wybrany próg przeciągania i zabezpiecz się przed rozpoczęciem drugiej sesji. - Utwórz
NSDraggingItemz zapisywacza do schowka działającego w pamięci, który zawiera identyfikator syntetycznego elementu. - Udostępnij wygenerowany obraz kształtu i ustaw jego ramkę przeciągania w układzie współrzędnych widoku źródłowego.
- Wywołaj
beginDraggingSession(with:event:source:), używając zachowanego zdarzenia naciśnięcia przycisku myszy i obiektu implementującegoNSDraggingSource. - Pokaż informację zwrotną dla źródła w
draggingSession(_:willBeginAt:)i bezwarunkowo przywróć wygląd wdraggingSession(_:endedAt:operation:). - Wyczyść zachowane zdarzenie wejściowe i dane sesji po zakończeniu śledzenia. Utrzymuj źródło i obiekt obsługujący jego wywołanie zwrotne przy życiu do zakończenia sesji.
Szczegóły te wynikają z jawnych warunków AppKit: metoda rozpoczynająca sesję wymaga inicjującego zdarzenia naciśnięcia przycisku myszy, ramki elementów przeciągania używają układu współrzędnych widoku, a źródło otrzymuje komunikaty cyklu życia. NSDraggingItem przyjmuje obiekt zgodny z NSPasteboardWriting; jego wygodna metoda przyjmująca pojedynczy obraz ustawia ramkę i zawartość przeciągania. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)
W przypadku tej interakcji w obrębie własnej aplikacji zwracaj .copy dla NSDraggingContext.withinApplication, a dla innych kontekstów pusty zestaw operacji. To jawna decyzja, by oferować wyłącznie lokalne kopie. AppKit pyta źródło, na jakie operacje zezwala, za pomocą draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)
Przywracaj wygląd przy każdym wyniku zakończenia, a nie tylko po udanym kopiowaniu. Zmiany modelu uzależnij od operacji faktycznie zaimplementowanej przez aplikację. Punkt końcowy wywołania zwrotnego jest podawany we współrzędnych ekranu; nie porównuj go bezpośrednio z lokalnymi granicami obszaru docelowego SwiftUI. draggingSession(_:endedAt:operation:)
Przezroczysty widok natywny umieszczony za istniejącym .draggable SwiftUI nie staje się źródłem tej sesji tylko dlatego, że jest zgodny z NSDraggingSource. Źródłem jest obiekt przekazany przy rozpoczęciu sesji. Wybierz jednego właściciela inicjowania i kończenia przeciągania, a natywny obszar przeciągania ogranicz do zamierzonego obszaru interakcji kształtu.
Dlaczego nie używać monitorowania zwolnienia przycisku myszy ani deinicjalizacji dostawcy?
Monitor zwolnienia przycisku myszy w całej aplikacji obserwuje przekazywanie zdarzeń wejściowych, co jest inną granicą niż zakończenie przeciągania. Apple dokumentuje, że lokalne monitory zdarzeń nie otrzymują zdarzeń przechwyconych przez zagnieżdżone pętle śledzenia. To ograniczenie sprawia, że monitor nie nadaje się na zamiennik udokumentowanego wywołania zwrotnego zakończenia po stronie źródła. addLocalMonitorForEvents(matching:handler:)
Podobnie deinit dostawcy elementu informuje o czasie życia obiektu. Swift wywołuje deinicjalizator bezpośrednio przed zwolnieniem instancji; ten kontrakt nie określa, kiedy zakończyło się przeciąganie. Używanie zniszczenia dostawcy jako sygnału zakończenia wiąże stan interfejsu z własnością referencyjną zamiast z cyklem życia przeciągania. Deinitialization
Dodanie drugiego DragGesture obok przeciągania systemowego wiąże się z tym samym problemem koncepcyjnym: jego stan należy do tego gestu. Używaj @GestureState, gdy interakcję obsługuje gest lokalny; gdy odpowiada za nią przeciąganie systemowe, używaj wywołania zwrotnego sesji źródłowej. GestureState
Weryfikacja poprawki za pomocą macierzy zakończeń
Użyj syntetycznego okręgu, aby osobno sprawdzić każdą ścieżkę. Są to oczekiwane zachowania tej implementacji, a nie zgłoszone wyniki testów.
| Interakcja | Oczekiwana informacja zwrotna dla źródła | Oczekiwany wynik dla obszaru docelowego |
|---|---|---|
| Upuszczenie okręgu wewnątrz obszaru przyjmującego | Powraca do normalnego wyglądu | Licznik zwiększa się o jeden |
| Zwolnienie przycisku nad pustym miejscem okna | Powraca do normalnego wyglądu | Licznik pozostaje bez zmian |
| Opuszczenie obszaru przyjmującego podczas dalszego przeciągania | Pozostaje przyciemnione | Podświetlenie obszaru docelowego znika |
| Ponowne wejście i zakończenie prawidłowego upuszczenia | Powraca do normalnego wyglądu po zakończeniu | Licznik zwiększa się o jeden |
| Zwolnienie poza oknem aplikacji bez akceptującego obszaru docelowego | Powraca do normalnego wyglądu | Licznik pozostaje bez zmian |
| Anulowanie aktywnego przeciągania | Powraca do normalnego wyglądu | Licznik pozostaje bez zmian |
| Powtórzenie kilku nieudanych przeciągnięć | Każda sesja przywraca normalny wygląd | Nie gromadzą się zaakceptowane kopie |
Jeśli źródło pozostaje przyciemnione, najpierw sprawdź, czy obserwator zakończenia otacza rzeczywiste źródło przeciągania i czy źródło nadal istnieje. Jeśli przykleja się tylko podświetlenie obszaru docelowego, sprawdź obsługę wejścia i wyjścia z obszaru oraz wspólne czyszczenie. Jeśli licznik zaakceptowanych elementów zmienia się przy odrzuconym upuszczeniu, sprawdź osobno walidację i modyfikację modelu, niezależnie od przywracania wyglądu.
Trwała poprawka polega na jawnym przypisaniu odpowiedzialności za zakończenie: źródło przywraca informację zwrotną dla całej sesji, obszar docelowy obsługuje własny stan najechania i zaakceptowaną zawartość, a czyszczenie po transferze odbywa się zgodnie z jego udokumentowanym cyklem życia. Po rozdzieleniu tych zadań upuszczenie poza wszystkimi obszarami docelowymi nie pozostawia stanu wizualnego oczekującego na wywołanie zwrotne obszaru docelowego.
Źródła
onDragSessionUpdated(_:) — Apple
DragSession.Phase — Apple
DragSession.Phase.ended(_:) — Apple
draggable(_:) — Apple
DropDelegate — Apple
GestureState — Apple
NSViewRepresentable — Apple
init(pasteboardWriter:) — Apple
setDraggingFrame(_:contents:) — Apple
Deinitialization — Swift.org