W obszarze roboczym Maca z nakładającymi się strefami upuszczania umieść natywną obsługę upuszczania na jednym stabilnym widoku nadrzędnym i wyznacz logiczny cel na podstawie bieżącego stanu modelu. Ponownie sprawdź, czy upuszczenie jest dozwolone, gdy je zatwierdzasz; odrzucone wywołanie zwrotne nie oznacza udokumentowanego przekazania obsługi innemu widokowi.
W tym artykule zbudujemy tę interakcję dla aplikacji zawierającej etykiety syntetycznych kształtów i dwa nakładające się panele. Konkretne zadanie polega na umożliwieniu odbioru upuszczanego elementu przez niższy panel, gdy górny przestaje je przyjmować, także wtedy, gdy jego uprawnienia zmienią się w trakcie przeciągania. Opisana niżej reguła trasowania to projekt aplikacji oparty na udokumentowanych wywołaniach zwrotnych.
Dlaczego zmiana stanu nie przekazuje niezawodnie przeciągania innemu celowi?
Najpierw rozdziel trzy kwestie: który natywny widok odbiera przeciąganie, który panel aplikacja uznaje za dostępny oraz który model otrzymuje element po zakończeniu operacji. Zmiana stanu może zmienić odpowiedź na drugie pytanie, ale nie wyznacza przez to nowej odpowiedzi na pierwsze.
Apple opisuje validateDrop(info:) w SwiftUI jako walidację przy wejściu zgodnego przeciąganego elementu do celu. dropUpdated(info:) opisuje jako aktualizację, gdy zwalidowany element przemieszcza się wewnątrz zmodyfikowanego widoku. Żaden z tych opisów nie gwarantuje ponownego wyszukiwania celu za każdym razem, gdy zmieni się niezwiązany stan widoku nadrzędnego. Walidacja w SwiftUI i aktualizacje upuszczania.
Granica AppKit jest opisana bardziej jednoznacznie: tylko jeden cel otrzymuje jednocześnie sekwencję komunikatów draggingUpdated(_:). Gdy dwa nakładające się widoki są prawidłowymi celami, komunikaty trafiają do najwyższego z nich, dopóki obraz nie zostanie upuszczony albo przeciągnięty poza ten widok. To opis trasowania w AppKit; nie ustanawia ogólnej zasady pierwszeństwa rodzeństwa w SwiftUI. Aktualizacje celu w AppKit.
Zwrócenie false ze starszej akcji upuszczania w SwiftUI zgłasza niepowodzenie operacji. Apple nie opisuje tej wartości jako żądania ponownej próby dostarczenia elementu do celu znajdującego się niżej. Podobnie zwrócenie nil z dropUpdated(info:) zachowuje ostatnią prawidłową propozycję albo domyślnie wybiera kopiowanie; nie oznacza „przekaż dalej”. Obsługa upuszczania oparta na domknięciu i zachowanie propozycji.
W praktyce oznacza to, że aplikacja musi jawnie określić regułę awaryjnego wyboru celu. W przypadku pojedynczego obszaru roboczego jeden natywny odbiornik może wybierać spośród kilku logicznych paneli bez polegania na natywnym przekazywaniu obsługi między nakładającymi się odbiornikami podrzędnymi.
Określ własność i regułę awaryjną przed dodaniem modyfikatorów
W tym przykładzie pomarańczowy panel znajduje się za niebieskim. Pomarańczowy zawsze przyjmuje przykładowe etykiety. Niebieski przyjmuje je tylko wtedy, gdy blueEnabled ma wartość prawda. Oczekiwane zachowanie:
| Położenie wskaźnika | Niebieski przyjmuje upuszczenia | Odbiorca logiczny |
|---|---|---|
| Obszar tylko pomarańczowego panelu | Dowolna wartość | Pomarańczowy |
| Część wspólna | Tak | Niebieski |
| Część wspólna | Nie | Pomarańczowy |
| Obszar tylko niebieskiego panelu | Nie | Brak |
| Poza oboma panelami | Dowolna wartość | Brak |
Ta tabela pomaga podejmować decyzje podczas implementacji. W szczególności wyłączenie niebieskiego panelu nie sprawia, że każdy punkt w jego prostokącie staje się celem pomarańczowego. Pomarańczowy otrzymuje element tylko wtedy, gdy wskaźnik znajduje się również wewnątrz prostokąta pomarańczowego panelu.
Przechowuj stan określający trasowanie nad panelami wizualnymi. Apple zaleca umieszczanie wartości stanu w najwyższym widoku, który jej potrzebuje, i udostępnianie jej widokom potomnym. W tym przypadku jeden widok nadrzędny posiada model zawierający informację o uprawnieniach, geometrii i odebranych elementach. Prostokąty podrzędne określają wygląd, a nie niezależne procedury obsługi upuszczania. Zarządzanie własnością stanu w SwiftUI.
Odczytuj model w chwili wywołania akcji upuszczania. Nie zapisuj wartości logicznej określającej uprawnienia ani identyfikatora celu przy wejściu, by później uznawać go za wiążący. Domknięcia w Swift mogą zachowywać wartości i referencje z kontekstu, w którym zostały utworzone; zachowana referencja może nadal wskazywać starszy model po zastąpieniu właściciela. Sprawdź, który obiekt faktycznie przechwytuje wywołanie zwrotne. Przechwytywanie wartości w domknięciach Swift.
Zastępowanie właściciela wymaga osobnej reguły: albo rozstrzygaj cel na podstawie nowego właściciela, albo odrzucaj operacje należące do poprzedniego. Przydatny wariant odrzucania przypisuje właścicielowi numer rewizji, zwiększa go przy zastąpieniu i porównuje przechwyconą rewizję przed zatwierdzeniem opóźnionej operacji. Chwilowa wartość „aktualnie podświetlony” nie powinna rozstrzygać o własności.
Zbuduj jedną stabilną powierzchnię upuszczania w SwiftUI
Poniższy kompletny przykład wymaga aplikacji SwiftUI na macOS z minimalną obsługiwaną wersją macOS 26 oraz pakietu SDK zawierającego dropDestination(for:isEnabled:action:). Apple opisuje to przeciążenie jako przyjmujące typ Transferable, flagę określającą, czy obsługa jest włączona, i akcję otrzymującą zdekodowane elementy wraz z obiektem DropSession. Cel zajmuje rozmiar i położenie zmodyfikowanego widoku. Aktualne API celu upuszczania.
Utwórz nowy projekt aplikacji macOS używający SwiftUI. Zastąp wygenerowaną deklarację aplikacji dwoma poniższymi blokami kodu, umieszczonymi kolejno w jednym pliku Swift. Razem tworzą jeden program. Wykorzystuje on wyłącznie ciągi znaków i prostokąty przechowywane w pamięci. Etykiety źródłowe pozostają dostępne po upuszczeniu, więc przykład działa jak operacja kopiowania.
Model i deterministyczne trasowanie
Model najpierw sprawdza niebieski panel, aby wyrazić wybrane pierwszeństwo wizualne. Zarówno renderowanie, jak i trasowanie korzystają z tych samych prostokątów. Metoda zatwierdzająca ponownie wyznacza odbiorcę na podstawie bieżących uprawnień i sprawdza całą partię elementów, zanim cokolwiek doda.
import SwiftUI
import Combine
@MainActor
final class CanvasModel: ObservableObject {
enum Zone: String {
case orange = "Orange"
case blue = "Blue"
}
let orangeRect = CGRect(x: 30, y: 30, width: 230, height: 170)
let blueRect = CGRect(x: 150, y: 100, width: 230, height: 170)
let samples = ["Circle", "Square"]
@Published var blueEnabled = true
@Published private(set) var orangeItems: [String] = []
@Published private(set) var blueItems: [String] = []
@Published private(set) var message = "Drag a sample onto a panel."
func recipient(at point: CGPoint) -> Zone? {
if blueEnabled && blueRect.contains(point) {
return .blue
}
if orangeRect.contains(point) {
return .orange
}
return nil
}
func receive(_ items: [String], at point: CGPoint, isLocal: Bool) {
guard isLocal,
!items.isEmpty,
items.allSatisfy({ samples.contains($0) }) else {
message = "No items added: use this window's sample labels."
return
}
guard let zone = recipient(at: point) else {
message = "No items added: this location has no eligible panel."
return
}
switch zone {
case .orange:
orangeItems.append(contentsOf: items)
case .blue:
blueItems.append(contentsOf: items)
}
message = "Added \(items.joined(separator: ", ")) to \(zone.rawValue)."
}
}
Aplikacja i odbiornik upuszczania
Prostokąt nadrzędny posiada jedyny modyfikator obsługi upuszczania. Żaden z kolorowych paneli nie rejestruje konkurencyjnego celu. DropSession udostępnia położenie w lokalnym układzie współrzędnych celu oraz obiekt localSession, który identyfikuje przeciąganie rozpoczęte w aplikacji. Model sprawdza także przykładowe wartości. Informacje o sesji upuszczania.
@main
struct DropRoutingDemoApp: App {
var body: some Scene {
WindowGroup {
CanvasDemo()
}
}
}
@MainActor
struct CanvasDemo: View {
@StateObject private var model = CanvasModel()
@State private var scheduledChange: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
ForEach(model.samples, id: \.self) { item in
Text(item)
.padding(10)
.background(Color.gray.opacity(0.15))
.draggable(item)
}
}
Toggle("Blue accepts drops", isOn: $model.blueEnabled)
Button("Toggle blue in 3 seconds") {
scheduledChange?.cancel()
scheduledChange = Task { @MainActor in
do {
try await Task.sleep(for: .seconds(3))
} catch {
return
}
model.blueEnabled.toggle()
}
}
ZStack(alignment: .topLeading) {
Rectangle().fill(Color.gray.opacity(0.08))
panel("Orange", rect: model.orangeRect,
color: .orange, opacity: 0.55)
panel("Blue", rect: model.blueRect,
color: .blue,
opacity: model.blueEnabled ? 0.55 : 0.15)
}
.frame(width: 420, height: 300)
.dropDestination(for: String.self, isEnabled: true) {
items, session in
model.receive(items, at: session.location,
isLocal: session.localSession != nil)
}
Text(model.message)
Text("Orange: \(model.orangeItems.joined(separator: ", "))")
Text("Blue: \(model.blueItems.joined(separator: ", "))")
}
.padding(20)
.onDisappear {
scheduledChange?.cancel()
}
}
private func panel(_ title: String, rect: CGRect,
color: Color, opacity: Double) -> some View {
Rectangle()
.fill(color.opacity(opacity))
.overlay(Text(title))
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
}
}
Modyfikator SwiftUI draggable(_:) instaluje gesty potrzebne do rozpoczęcia przeciągania za pomocą danych wejściowych użytkownika. Przycisk z opóźnieniem zmienia wyłącznie stan uprawnień w przykładzie; nie rozpoczyna, nie uruchamia ponownie ani nie przekazuje sesji przeciągania. Zachowanie źródła przeciągania w SwiftUI.
Naciśnij Toggle blue in 3 seconds, a następnie przeciągnij Circle do części wspólnej i przytrzymaj, aż niebieski panel zniknie. Upuść w części wspólnej: zamierzonym odbiorcą jest panel pomarańczowy. Jeśli niebieski panel jest wyłączony, upuszczenie w jego niepokrywającym się obszarze nie zmieni żadnej tablicy elementów, a komunikat wyjaśni dlaczego.
Ten odbiornik pozostaje włączony na całym obszarze roboczym, aby mógł zastosować pełną regułę trasowania. Dlatego przykład nie zapewnia natywnego kursora odrzucenia ani podświetlenia po najechaniu zależnych od położenia. Komunikat o akceptacji przez aplikację pojawia się po upuszczeniu. Jeśli produkt wymaga takiej informacji zwrotnej, dodaj osobno obsługę propozycji i najechania.
Kod sprawdzono w odniesieniu do pobranych deklaracji API. Nie został tutaj skompilowany ani uruchomiony, ponieważ dostępne środowisko nie zawiera narzędzi Swift ani środowiska wykonawczego macOS.
Osobno obsłuż aktualizacje po najechaniu i starsze wersje systemu
Jeśli obsługujesz systemy bez nowszego przeciążenia, zachowaj tę samą regułę modelu i geometrii, wybierając dostępne API odbioru. Starsze dropDestination(for:action:isTargeted:) przekazuje zdekodowane elementy i lokalny punkt; wynik logiczny informuje o powodzeniu. Jego domknięcie isTargeted opisuje wejście do natywnego obszaru celu i wyjście z niego, którym w tym projekcie jest cały obszar roboczy. Nie wskazuje logicznego panelu znajdującego się pod wskaźnikiem. Umowa starszego celu upuszczania.
Aby jawnie obsługiwać walidację i propozycje operacji, onDrop(of:delegate:) instaluje obiekt DropDelegate. Użyj validateDrop(info:) do początkowego sprawdzenia uprawnień, dropUpdated(info:) do aktualnych propozycji, a performDrop(info:) do końcowej walidacji i włączenia elementów. Zwrócenie .forbidden oznacza niedozwolone działanie w danym miejscu; nie dokumentuje przekazania obsługi innemu natywnemu odbiornikowi. Cele oparte na delegacie i operacje upuszczania.
Korzystanie z providerów wiąże się z ważnym ograniczeniem czasowym: DropInfo.itemProviders(for:) jest prawidłowe tylko podczas performDrop. Rozpocznij wczytywanie zawartości providera, zanim ta metoda zwróci wynik. Zakończenie może nastąpić później, więc przed zmianą modelu ponownie sprawdź tożsamość celu, jego rewizję i aktualne uprawnienia. Zgłoś odrzucenie lub niepowodzenie wczytywania zamiast po cichu odrzucać wynik. Dostęp do providerów i wymogi dotyczące kończenia operacji upuszczania.
Informacje o najechaniu przechowuj oddzielnie od odebranych elementów, w postaci przejściowych danych sesji. Dokumentacja nowszego DropSession odsyła do onDropSessionUpdated(_:) w celu obserwowania zmian fazy. Nie zakładaj, że zmiana stanu modelu wywoła kolejną aktualizację położenia wskaźnika; jeśli masz już prawidłowe położenie najechania, przelicz logiczne podświetlenie również po zmianie uprawnień. Końcowa akceptacja nadal powinna ponownie wywołać funkcję trasowania. Cykl życia sesji upuszczania.
Kiedy obszar roboczy powinien korzystać z AppKit?
Użyj celu AppKit, gdy wymagana interakcja zależy od udokumentowanych, cyklicznych aktualizacji natywnych przy nieruchomym wskaźniku. AppKit udostępnia właściwość wantsPeriodicDraggingUpdates() do sterowania tym zachowaniem: zwrócenie false ogranicza aktualizacje do ruchu myszy lub zmiany modyfikatorów; w przeciwnym razie aktualizacje cykliczne następują także wtedy, gdy nic się nie zmienia. Dzięki temu można w udokumentowany sposób ponownie ocenić bieżącą operację podczas nieruchomego przeciągania. Cykliczne aktualizacje przeciągania.
Właściwa architektura nadal używa jednego celu dla całego obszaru roboczego. Zarejestruj przyjmowane typy schowka, wyznaczaj logiczne panele w metodach draggingEntered(_:) i draggingUpdated(_:), sprawdzaj akceptację przy upuszczaniu i włączaj element w performDragOperation(_:). Apple opisuje wprost dostosowywanie operacji do obszarów grafiki lub tekstu zawartych w jednym widoku celu. Cykl życia celu w AppKit i operacje zależne od położenia.
Podczas osadzania tego celu za pomocą NSViewRepresentable aktualizuj referencję do modelu, konfigurację uprawnień i wywołania zwrotne w updateNSView(_:context:). Samo utworzenie widoku nie synchronizuje późniejszych zmian właściciela; Apple wskazuje metodę aktualizacji jako miejsce, w którym widok AppKit należy doprowadzić do zgodności ze stanem SwiftUI. Aktualizowanie widoku AppKit.
Zachowaj też granicę zdarzeń źródłowych. AppKitowa metoda beginDraggingSession(with:event:source:) rozpoczyna sesję na podstawie przeciąganych elementów i początkowego zdarzenia naciśnięcia przycisku myszy. Nie służy do zmiany docelowego odbiornika. Pozostaw istniejące źródło SwiftUI, jeśli spełnia wymagania; użyj źródła AppKit tylko wtedy, gdy potrzebujesz również przejąć obsługę interakcji po stronie źródła. Rozpoczynanie przeciągania w AppKit.
Weryfikuj zmiany stanu w miejscach wpływających na własność
Przeprowadź poniższe ręczne sprawdzenia na obsługiwanych wersjach macOS. Pozwolą one odróżnić prawidłowe trasowanie od zmiany wizualnej, która tylko pozornie działa:
- Upuść element w części wspólnej, gdy niebieski panel jest włączony: element pojawia się tylko w jego tablicy.
- Wyłącz niebieski panel przed rozpoczęciem przeciągania: upuszczenie w części wspólnej jest kierowane do pomarańczowego.
- Wyłącz niebieski panel podczas nieruchomego przeciągania: końcowa akceptacja uwzględnia stan wyłączony.
- Upuść w obszarze należącym tylko do niebieskiego panelu, gdy jest wyłączony: obie tablice pozostają bez zmian, a komunikat wyjaśnia wynik.
- Włącz ponownie niebieski panel podczas przeciągania: w chwili zatwierdzania część wspólna jest kierowana do niebieskiego.
- Anuluj przeciąganie: żadna tablica się nie zmienia.
- Jeśli aplikacja zastępuje właścicieli obszaru roboczego, zastąp jednego z nich podczas przeciągania i zweryfikuj wybraną regułę zastępowania właściciela.
- Jeśli implementacja asynchronicznie wczytuje dane od providerów, zmień właściciela przed zakończeniem i zweryfikuj sprawdzanie rewizji oraz widoczne zgłaszanie błędów.
W przypadku przewijanego, powiększanego lub zmienianego rozmiaru obszaru roboczego rozwiń ten projekt, przeliczając granice każdego panelu i położenie upuszczenia na ten sam układ współrzędnych. Stałe prostokąty w tym przykładzie pozwalają uniknąć takiego przeliczania; skopiowanie ich współrzędnych do przekształconego układu zepsułoby regułę trasowania.
Stabilny natywny odbiornik i funkcja trasowania korzystająca z bieżącego stanu zapewniają jednoznaczny wynik: element otrzymuje najwyższy dostępny panel, w nakładającym się obszarze trafia do dostępnego niższego panelu, a położenie poza panelami nie zmienia modelu. Zastosuj tę samą regułę do informacji zwrotnej o najechaniu i końcowej akceptacji, przy czym ta ostatnia powinna przeprowadzić własne sprawdzenie.
Źródła
validateDrop(info:) — Apple.
dropUpdated(info:) — Apple.
draggingUpdated(_:) — Apple.
Stan — Apple.
Domknięcia — Swift.org.
DropSession — Apple.
draggable(_:) — Apple.
onDrop(of:delegate:) — Apple.
DropOperation — Apple.
itemProviders(for:) — Apple.
performDrop(info:) — Apple.
wantsPeriodicDraggingUpdates() — Apple.
NSDraggingDestination — Apple.
updateNSView(_:context:) — Apple.