Podczas przeciągania i upuszczania w aplikacji na Macu źródło deklaruje obsługiwane operacje, miejsce docelowe wybiera jedną z nich, a model realizuje wynik. W SwiftUI na macOS 26 lub nowszym skonfiguruj oba końce i usuń oryginał dopiero po zakończonym przeniesieniu.
W tym artykule zbudujemy niewielką interakcję na komputerze: przeciągniemy syntetyczne koło do jednego z dwóch miejsc: Copy here lub Move here. Wszystkie dane pozostają w pamięci. Przykład jasno określa operację, tożsamość elementu i czyszczenie źródła, a następnie pokazuje, jak te same obowiązki realizować w AppKit.
Co źródło i miejsce docelowe muszą uzgodnić?
Przenoszony między widokami ładunek zawiera dane. Kopiowanie lub przenoszenie określa, co stanie się z oryginałem. To odrębne decyzje. Dokumentacja Apple dotycząca SwiftUI informuje, że przeciąganie domyślnie obsługuje kopiowanie; włączenie przenoszenia dodaje kolejną obsługiwaną operację. Następnie miejsce docelowe wybiera sposób odebrania elementu. DragConfiguration
Podziel projekt interakcji na trzy obowiązki:
| Obowiązek | Kopiowanie | Przenoszenie |
|---|---|---|
| Możliwości źródła | Udostępnij kopiowanie | Udostępnij przenoszenie |
| Zatwierdzenie w miejscu docelowym | Wstaw niezależny element | Odbierz istniejący element |
| Zakończenie po stronie źródła | Zachowaj oryginał | Usuń oryginał po zakończonym przeniesieniu |
W przykładzie z kołem niezależna kopia otrzymuje nowy identyfikator. Przeniesienie zachowuje identyfikator, zmieniając położenie elementu. To decyzja projektowa aplikacji zgodna z wymaganiem Swifta, aby identyfikowalna jednostka miała stabilną tożsamość; Swift pozostawia aplikacji określenie zakresu i czasu trwania tej tożsamości. Identifiable
W praktyce zwrócenie operacji przeniesienia nie przenosi elementu między tablicami. Nadal trzeba wstawić go w miejscu docelowym i usunąć ze źródła. Apple pokazuje w dokumentacji, jak obserwować zakończoną operację przeciągania, aby poinformować model, że ma usunąć element ze źródła. onDragSessionUpdated(_:)
Których interfejsów API SwiftUI powinna używać aplikacja na Maca?
W tej implementacji ustaw minimalny obsługiwany system na macOS 26 lub nowszy i użyj SDK zawierającego udokumentowane modyfikatory konfiguracji. Dodaj draggable(_:) do źródła, dragConfiguration(_:), aby zadeklarować jego operacje, oraz dropConfiguration(_:), aby wybrać operację w miejscu docelowym. Dokumentacja podaje, że oba modyfikatory konfiguracji są dostępne w macOS 26. dragConfiguration(_:), dropConfiguration(_:)
Obecne przeciążenie dropDestination(for:isEnabled:action:) przekazuje do akcji obiekt DropSession. Akcja zwraca Void. Różni się to od starszego przeciążenia, którego akcja otrzymuje położenie i zwraca wartość logiczną. W przykładzie jawnie określ isEnabled:, aby wskazać zamierzone przeciążenie. dropDestination(for:isEnabled:action:)
W miejscu docelowym sprawdź suggestedOperations przed wybraniem operacji. Dla miejsca docelowego, które ogólnie preferuje przenoszenie, wybierz przenoszenie, jeśli jest dostępne, w przeciwnym razie kopiowanie, jeśli jest dostępne, a jeśli żadna z tych operacji nie jest dostępna, wybierz zakazaną. Udokumentowany przykład Apple stosuje ten schemat awaryjny: najpierw przenoszenie, potem kopiowanie. Apple zaznacza też, że zamknięcie konfiguracyjne jest często wywoływane, więc powinno działać szybko. dropConfiguration(_:)
Nasze dwa miejsca docelowe mają celowo przypisane stałe znaczenia. Copy here akceptuje kopiowanie, a Move here — przenoszenie. Miejsce docelowe zwraca zakazaną operację, gdy przypisana mu operacja nie jest dostępna. Dzięki temu wynik jest przewidywalny i nie wymaga używania modyfikatora klawiatury.
Zbuduj kompletny przykład kopiowania i przenoszenia w pamięci
Utwórz aplikację SwiftUI na macOS, ustaw minimalny obsługiwany system na macOS 26 lub nowszy i zastąp wygenerowane deklaracje aplikacji oraz widoku zawartości poniższym pojedynczym plikiem. Przykład przesyła wbudowany ciąg znaków zawierający identyfikator elementu, więc nie wymaga niestandardowego typu transferable ani deklaracji niestandardowego typu. Apple dokumentuje zarówno ładunki transferable dla źródeł przeciągania, jak i ładunki tekstowe dla miejsc docelowych upuszczania. draggable(_:), dropDestination(for:isEnabled:action:)
To kompletny, minimalny przykład aplikacji przeznaczony dla jednego otwartego okna i jednego elementu źródłowego. Sprawdziłem odwołania do API w pobranej dokumentacji; nie mogłem tutaj skompilować ani uruchomić przykładu, ponieważ nie są dostępne Swift ani SDK macOS.
import SwiftUI
import Foundation
@main
struct CopyMoveDemoApp: App {
var body: some Scene {
WindowGroup {
CopyMoveDemo()
}
}
}
struct CopyMoveDemo: View {
@State private var sourceID = UUID()
@State private var sourcePresent = true
@State private var copiedIDs: [UUID] = []
@State private var movedIDs: [UUID] = []
@State private var moveReceived = false
private var sourceConfiguration: DragConfiguration {
DragConfiguration(
operationsWithinApp: .init(allowMove: true),
operationsOutsideApp: .init(
allowCopy: false,
allowMove: false,
allowDelete: false
)
)
}
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle to a target")
.font(.headline)
if sourcePresent {
Circle()
.fill(.blue)
.frame(width: 90, height: 90)
.overlay(Text("Circle").foregroundStyle(.white))
.draggable(sourceID.uuidString)
.dragConfiguration(sourceConfiguration)
.onDragSessionUpdated { session in
if session.phase == .initial {
moveReceived = false
}
if session.phase == .ended(.move), moveReceived {
sourcePresent = false
}
if case .ended = session.phase {
moveReceived = false
}
}
} else {
Text("Original moved")
.frame(width: 90, height: 90)
}
HStack(alignment: .top, spacing: 20) {
target(moving: false)
target(moving: true)
}
}
.padding(24)
.frame(minWidth: 520, minHeight: 320)
}
private func target(moving: Bool) -> some View {
let operation: DropOperation = moving ? .move : .copy
let ids = moving ? movedIDs : copiedIDs
return VStack(spacing: 10) {
Text(moving ? "Move here" : "Copy here")
.font(.headline)
ForEach(ids, id: \.self) { id in
Text("Circle \(id.uuidString.prefix(4))")
}
Spacer(minLength: 20)
}
.frame(width: 210, height: 150)
.background(.blue.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(for: String.self, isEnabled: true) {
tokens, session in
guard session.localSession != nil,
sourcePresent,
tokens == [sourceID.uuidString] else {
return
}
if moving {
guard !movedIDs.contains(sourceID) else { return }
movedIDs.append(sourceID)
moveReceived = true
} else {
copiedIDs.append(UUID())
}
}
.dropConfiguration { session in
guard sourcePresent,
session.localSession != nil,
session.itemsCount == 1,
session.suggestedOperations.contains(operation) else {
return DropConfiguration(operation: .forbidden)
}
return DropConfiguration(operation: operation)
}
}
}
Konfiguracja źródła udostępnia kopiowanie i przenoszenie w obrębie aplikacji. Operacje poza aplikacją są wyłączone za pomocą inicjalizatora dla operacji poza aplikacją dostępnego w macOS. Miejsce docelowe sprawdza również localSession, który według dokumentacji Apple nie występuje, gdy przeciąganie rozpoczęło się poza aplikacją. Dzięki tym ustawieniom przykład skupia się na elemencie przechowywanym w pamięci okna aplikacji. DragConfiguration.OperationsWithinApp, init(allowCopy:allowMove:allowDelete:), DropSession
Akcja upuszczenia sprawdza dokładny token, zanim cokolwiek wstawi. Kopiowanie dodaje nowy identyfikator. Przenoszenie dodaje identyfikator oryginału i zapisuje informację, że miejsce docelowe go odebrało. Następnie źródło usuwa wyświetlany oryginał tylko wtedy, gdy sesja zakończy się przeniesieniem i istnieje potwierdzenie jego odbioru. Potwierdzenie to zabezpieczenie na poziomie aplikacji na potrzeby tej ograniczonej demonstracji, a nie funkcja transakcyjna SwiftUI.
Aktualna deklaracja faz przeciągania Apple zawiera initial, active, ending, ended i dataTransferCompleted. W przykładzie użyto udokumentowanego przypadku ended(DropOperation) dla wyniku operacji. Źródło nie jest usuwane podczas najeżdżania ani na wcześniejszym etapie kończenia. DragSession.Phase
Jawnie określ właściciela i granice gestu
Ciąg znaków jest tokenem wyszukiwania, a nie pełnym modelem koła. Miejsce docelowe wyszukuje go w stanie źródłowym należącym do tego widoku. W przypadku wielu dokumentów lub okien zastąp to lokalne wyszukiwanie jawnie wskazanym właścicielem modelu i potwierdzeniem powiązanym z konkretną sesją przeciągania oraz elementami. Sam fakt, że przeciąganie rozpoczęło się wewnątrz aplikacji, nie określa, do którego dokumentu należy element.
W przypadku przeciągania wielu elementów używaj stabilnych identyfikatorów modelu i pobieraj identyfikatory przeciąganych elementów z sesji, gdy są dostępne. Apple udostępnia draggedItemIDs(for:) dla identyfikowalnych ładunków lub przekazanych osobno identyfikatorów. Unikaj usuwania elementu, który akurat jest zaznaczony w chwili nadejścia powiadomienia o zakończeniu; zaznaczenie i tożsamość przeciąganego elementu to różne dane wejściowe. DragSession
Pozwól, aby modyfikator draggable(_:) SwiftUI instalował interakcję przeciągania. Tworzenie ładunku nie powinno usuwać danych z modelu. W dokumentacji Apple podano, że modyfikator dodaje odpowiednie gesty i zalecono, aby nie przekazywać zamknięcia eksportującego na aktora głównego. draggable(_:)
Jak działa to samo uzgodnienie w AppKit?
Użyj AppKit, gdy wymaga tego minimalna wersja wdrożeniowa lub architektura istniejących widoków i potrzebujesz jawnego cyklu życia przeciągania. Protokół źródła deklaruje dozwolone operacje za pomocą draggingSession(_:sourceOperationMaskFor:). Kontekst pozwala odróżnić miejsca docelowe w aplikacji od tych poza nią. W powyższej interakcji udostępnij kopiowanie i przenoszenie w aplikacji, a poza nią nie udostępniaj żadnej operacji. draggingSession(_:sourceOperationMaskFor:)
Miejsce docelowe rejestruje odpowiedni typ schowka, a następnie wybiera operację w draggingEntered(_:) i w razie potrzeby aktualizuje wybór w draggingUpdated(_:). Apple wymaga, aby miejsce docelowe uwzględniało część wspólną operacji źródła z własnymi obsługiwanymi operacjami i typami danych. Zwróć jedną operację albo żadną, jeśli nie ma akceptowalnej części wspólnej. NSDraggingDestination, draggingEntered(_:)
Wykonaj wstawienie w performDragOperation(_:), gdy przygotowanie zakończy się powodzeniem. Zwróć true tylko wtedy, gdy miejsce docelowe akceptuje dane. Źródło otrzymuje wynik zakończonej operacji przez draggingSession(_:endedAt:operation:); wykorzystaj go wraz z zasadami własności w modelu, aby zakończyć usuwanie po stronie źródła w przypadku przeniesienia. performDragOperation(_:), draggingSession(_:endedAt:operation:)
Jeśli osadzasz obszar przeciągania AppKit w interfejsie SwiftUI, pozostaw obsługę interakcji AppKit temu obszarowi. beginDraggingSession(with:event:source:) przyjmuje zdarzenie naciśnięcia myszy, które rozpoczyna przeciąganie, a ramki przeciąganych elementów korzystają z układu współrzędnych widoku. Zachowaj to powiązanie między zdarzeniem a układem współrzędnych zamiast tworzyć sztuczne zdarzenie na podstawie późniejszej wartości gestu SwiftUI. beginDraggingSession(with:event:source:)
Starszy kod miejsca docelowego w SwiftUI może zwracać DropProposal(operation: .move) z DropDelegate.dropUpdated(info:). Wyraża to propozycję miejsca docelowego. Nie zastępuje konfiguracji możliwości źródła, obserwacji zakończonej operacji ani zmian w modelu. Gdy nowsze API źródeł SwiftUI są niedostępne, a potrzebujesz jawnie kontrolować oba końce, tę możliwość zapewnia cykl życia źródła i miejsca docelowego w AppKit. dropUpdated(info:), NSDraggingSource
Zweryfikuj operację i model jednocześnie
W przypadku przykładu użyj poniższej listy oczekiwanych stanów. Są to warunki akceptacji wynikające z implementacji, a nie zgłoszone wyniki testów:
| Działanie | Oryginał | Miejsce docelowe kopiowania | Miejsce docelowe przenoszenia |
|---|---|---|---|
Upuść raz na Copy here |
Pozostaje | Zyskuje jeden nowy identyfikator | Bez zmian |
| Skopiuj ponownie | Pozostaje | Zyskuje kolejny nowy identyfikator | Bez zmian |
Upuść na Move here |
Znika po zakończonym przeniesieniu | Bez zmian | Otrzymuje identyfikator oryginału |
| Anuluj lub zwolnij poza oboma miejscami docelowymi | Pozostaje | Bez zmian | Bez zmian |
Jeśli działanie odbiega od oczekiwań, sprawdź kolejno etapy: możliwości źródła, operację w miejscu docelowym, wstawienie w miejscu docelowym i zakończenie po stronie źródła. Zduplikowany oryginał po przeniesieniu wskazuje na brak czyszczenia źródła. Usunięcie dwóch elementów wskazuje na konkurujących właścicieli modelu. Nieoczekiwane kopiowanie wskazuje, że źródło nie udostępniło przenoszenia albo miejsce docelowe wybrało kopiowanie.
Pamiętaj, na czym polega ostateczna odpowiedzialność: uzgodniona operacja opisuje transfer, a model określa położenie i tożsamość elementu. Udostępnij obsługiwane operacje, wybierz dozwoloną operację w miejscu docelowym i wykonaj każdą zmianę modelu raz, w odpowiednim punkcie cyklu życia.
Źródła
DragConfiguration — Apple
Identifiable — Apple
onDragSessionUpdated(_:) — Apple
dragConfiguration(_:) — Apple
dropConfiguration(_:) — Apple
draggable(_:) — Apple
DropSession — Apple
DragSession.Phase — Apple
DragSession — Apple
NSDraggingDestination — Apple
draggingEntered(_:) — Apple
performDragOperation(_:) — Apple
dropUpdated(info:) — Apple
NSDraggingSource — Apple