Aby kopiować syntetyczne kształty między dwoma panelami aplikacji na Maca, zacznij od typowanego mechanizmu przeciągania i upuszczania w SwiftUI. Wybierz dostawców elementów, gdy wymaga ich istniejący interfejs dostawcy lub wcześniejsza wersja docelowa macOS. Zdefiniuj ładunek, reguły akceptacji i właściciela modelu, zanim wybierzesz któreś z tych API.
Określ, co oznacza udane upuszczenie
Zadanie użytkownika jest tu konkretne: umożliwić przeciągnięcie syntetycznego koła lub kwadratu z palety do kolekcji w tej samej aplikacji na Maca. Miejsce docelowe tworzy nowy element; paleta pozostaje bez zmian. Wszystko znajduje się w pamięci, a zamknięcie okna usuwa kolekcję.
Apple zaleca używanie elementów Transferable do przeciągania i upuszczania w Swift. Dokumentacja obsługuje też modyfikatory oparte na dostawcach, więc wybór zależy od wymagań dotyczących wdrożenia i interfejsu, z którym trzeba się zintegrować. Przeciąganie i upuszczanie — Apple.
Przed wdrożeniem widoków zapisz kontrakt transferu:
| Pytanie dotyczące kontraktu | Decyzja w tym przykładzie |
|---|---|
| Co przekracza granicę? | Wersjonowana wartość opisująca rodzaj kształtu |
| Kto może odebrać reprezentację? | Tylko aplikacja źródłowa |
| Co robi udane upuszczenie? | Dodaje jeden nowy element do kolekcji z nowym identyfikatorem |
| Kto odpowiada za mutację? | Stan kolekcji widoku docelowego |
| Co dzieje się po anulowaniu lub odrzuceniu? | Model się nie zmienia |
| Co oznacza miejsce upuszczenia? | W tym przypadku nic; zaakceptowane elementy są dodawane na końcu w kolejności |
To decyzje projektowe dotyczące aplikacji. Podgląd przeciąganego elementu przedstawia ładunek, a miejsce docelowe określa, jak ładunek zmienia model. Rozdzielenie tych odpowiedzialności pozwala zmienić sposób przekazywania danych bez przepisywania reguł kolekcji.
Wybierz najmniejsze API, które wyraża kontrakt
| Sytuacja | Punkt wyjścia | Powód |
|---|---|---|
| Źródło i miejsce docelowe w SwiftUI, macOS 13 lub nowszy | Transferable, .draggable i typowany .dropDestination |
Bezpośrednio określa akceptowany typ modelu |
Istniejąca integracja z NSItemProvider |
.onDrag i .onDrop |
Zachowuje jawne rejestrowanie reprezentacji i ich ładowanie |
| Wersja docelowa wcześniejsza niż macOS 13 | Sprawdź dostępność modyfikatorów dostawcy dla tej wersji | Wymagana wersja typowana to macOS 13; użyte poniżej przeciążenie dostawcy wymaga macOS 11 |
| Istniejący widok AppKit obsługuje interakcję myszy | API przeciągania AppKit | Zachowuje obsługę zdarzeń widoku i cykl życia sesji przeciągania |
Apple dokumentuje Transferable i pierwotne typowane miejsce docelowe przeciągania od macOS 13. Przeciążenie onDrop(of:isTargeted:perform:) oparte na dostawcy, przyjmujące tablicę wartości UTType, jest dostępne od macOS 11. Sprawdzaj dostępność konkretnego przeciążenia, zamiast zakładać, że każde API przeciągania i upuszczania ma tę samą minimalną wersję. Transferable, dropDestination(for:action:isTargeted:), onDrop(of:isTargeted:perform:).
Oba podejścia pozwalają ograniczyć widoczność do własnej aplikacji. W przypadku typowanej reprezentacji ustaw .visibility(.ownProcess); Apple definiuje ten poziom jako widoczność wyłącznie w aplikacji źródłowej. Wybór dostawcy elementów nie jest więc konieczny tylko po to, by transfer odbywał się w obrębie aplikacji. visibility(_:), ownProcess.
Zbuduj interakcję kopiowania z użyciem typowanego API
Ten kompletny przykład składa się z dwóch plików Swift w nowym celu aplikacji macOS SwiftUI. Ustaw wersję docelową na macOS 13 lub nowszą i użyj trybu języka Swift 5 jako podstawy. Zastąp wygenerowany punkt wejścia aplikacji drugim plikiem; pozostaw tylko jedną deklarację @main. Kod został sprawdzony, ale nie został tu skompilowany, ponieważ dostępna powłoka nie zawiera narzędzi Swift.
Zadeklaruj ładunek i jego typ zawartości
Dodaj deklarację eksportowanego typu do listy właściwości informacji celu. W edytorze list właściwości Xcode utwórz UTExportedTypeDeclarations jako tablicę z jednym słownikiem. Ustaw UTTypeIdentifier tego słownika na com.example.shape-transfer, UTTypeDescription na Synthetic Shape Transfer, a UTTypeConformsTo jako tablicę zawierającą public.data. Ten identyfikator jest przykładowy; użyj unikatowego identyfikatora swojej aplikacji.
Deklaracja Swift i identyfikator na liście właściwości muszą być zgodne. Apple wymaga odpowiednich wpisów na liście właściwości dla niestandardowych typów uniform i objaśnia deklaracje eksportowane dla formatów definiowanych przez aplikację. Ten przykład przesyła dane, a nie pliki, więc nie dodaje rozszerzenia nazwy pliku. Definiowanie typów plików i danych dla aplikacji.
Utwórz plik ShapePayload.swift:
import Foundation
import CoreTransferable
import UniformTypeIdentifiers
extension UTType {
static let shapeTransfer = UTType(
exportedAs: "com.example.shape-transfer",
conformingTo: .data
)
}
enum ShapeKind: String, Codable, Sendable {
case circle = "Circle"
case square = "Square"
}
struct ShapePayload: Codable, Sendable, Transferable {
let version: Int
let kind: ShapeKind
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .shapeTransfer)
.visibility(.ownProcess)
}
}
struct PlacedShape: Identifiable {
let id = UUID()
let kind: ShapeKind
}
CodableRepresentation zapewnia konwersję między ładunkiem a danymi transferowalnymi. Ładunek zawiera wartość, a nie widok ani odwołanie do mutowalnej kolekcji. Jawna zgodność z Sendable wynika z reguły Swift, zgodnie z którą przechowywane składowe struktury oznaczonej jako sendable również muszą być sendable. Transferable, Sendable.
Celowo użyto tylko jednej reprezentacji. Tekstowy odpowiednik dodałby inny sposób użycia elementu, który nie służy temu kontraktowi. Apple dokumentuje kolejność reprezentacji i formaty zapasowe; dodaj je tylko wtedy, gdy odbiorca faktycznie ich potrzebuje. Przekształcanie widoku w źródło przeciągania.
Zaakceptuj ładunek i zatwierdź jedną zmianę kolekcji
Utwórz plik ShapeTransferApp.swift:
import SwiftUI
@main
struct ShapeTransferApp: App {
var body: some Scene {
WindowGroup {
ShapeTransferView()
}
}
}
@MainActor
struct ShapeTransferView: View {
@State private var placed: [PlacedShape] = []
@State private var targeted = false
@State private var message = "Drag a shape into Collection."
var body: some View {
HStack(alignment: .top, spacing: 24) {
VStack(spacing: 16) {
Text("Palette").font(.headline)
paletteItem(.circle)
paletteItem(.square)
}
.frame(width: 160)
VStack(alignment: .leading, spacing: 12) {
Text("Collection").font(.headline)
Text(message)
ForEach(placed) { item in
Label(item.kind.rawValue,
systemImage: symbol(for: item.kind))
}
Spacer(minLength: 20)
}
.padding()
.frame(width: 300, height: 320, alignment: .topLeading)
.background(targeted ? Color.blue.opacity(0.15)
: Color.gray.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(
for: ShapePayload.self,
action: { payloads, _ in
guard !payloads.isEmpty,
payloads.allSatisfy({ $0.version == 1 })
else {
message = "Drop rejected: unsupported payload."
return false
}
let additions = payloads.map {
PlacedShape(kind: $0.kind)
}
placed.append(contentsOf: additions)
message = "Added \(additions.count) shape(s)."
return true
},
isTargeted: { targeted = $0 }
)
}
.padding(24)
}
private func paletteItem(_ kind: ShapeKind) -> some View {
Label(kind.rawValue, systemImage: symbol(for: kind))
.padding()
.background(Color.gray.opacity(0.12))
.draggable(ShapePayload(version: 1, kind: kind))
}
private func symbol(for kind: ShapeKind) -> String {
switch kind {
case .circle: return "circle.fill"
case .square: return "square.fill"
}
}
}
Jawne etykiety action: i isTargeted: wybierają pierwotne przeciążenie typowanego miejsca docelowego. Apple określa jego wartość logiczną jako powodzenie lub niepowodzenie, a argument lokalizacji jako współrzędne w układzie widoku docelowego. W tym przykładzie lokalizacja jest celowo pomijana, ponieważ kolekcja dodaje kształty na końcu zamiast ustawiać je w określonych miejscach. dropDestination(for:action:isTargeted:).
Reguła akceptacji sprawdza każdy dostarczony ładunek, zanim cokolwiek zostanie dodane. Nieobsługiwana wersja powoduje odrzucenie całej dostarczonej partii; każdy poprawny ładunek otrzymuje nowy identyfikator miejsca docelowego. Ponowne przeciągnięcie tego samego elementu z palety tworzy kolejną kopię. Ta reguła dotyczy wartości dostarczonych do zamknięcia; błędy dekodowania występują, zanim wartości staną się obiektami ShapePayload.
Stan wskazujący miejsce docelowe zmienia wyłącznie prezentację. Nie wstawia elementów ani niczego nie usuwa ze źródła. To rozdzielenie daje też prosty wynik anulowania: bez zaakceptowanego upuszczenia kolekcja pozostaje bez zmian.
Użyj dostawców elementów, gdy wymaga tego integracja
Implementacja oparta na dostawcy może przenosić tę samą wersję i rodzaj kształtu. Zachowaj niezmieniony kontrakt i zastąp sposób przekazywania danych, wykonując następujące kroki:
- W zamknięciu
.onDragutwórzNSItemProvideri zarejestruj reprezentację danych pod tym samym niestandardowym identyfikatorem typu, używając widoczności.ownProcess. - Przechwyć niezmienną wartość ładunku lub przygotowane bajty w procedurze obsługi ładowania. Zakoduj tę wartość i wywołaj procedurę zakończenia reprezentacji, przekazując dane albo błąd.
- Skonfiguruj
.onDrop, aby akceptował niestandardowy typ. - Wewnątrz akcji upuszczania od razu rozpocznij ładowanie każdego zaakceptowanego dostawcy za pomocą
loadDataRepresentation(forTypeIdentifier:completionHandler:). - Zdekoduj zwrócone dane, sprawdź wersję i zbierz wyniki. Zmodyfikuj kolekcję na głównym aktorze dopiero po uzyskaniu wymaganych wyników.
Apple dokumentuje jawne rejestrowanie danych i widoczność ograniczoną do własnej aplikacji dla dostawców. Metoda ładowania działa asynchronicznie i zwraca obiekt postępu. Zgodny identyfikator typu oznacza, że można podjąć próbę ładowania; nie gwarantuje, że ładowanie i dekodowanie zakończą się powodzeniem. registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:), NSItemProviderRepresentationVisibility.ownProcess, loadDataRepresentation(forTypeIdentifier:completionHandler:).
Istotne są dwie granice czasowe. Po pierwsze, Apple wskazuje, że ładowanie dostawcy musi rozpocząć się w akcji .onDrop, choć może zakończyć się później. Przeniesienie początkowego wywołania ładowania do późniejszego zadania działającego na innym aktorze narusza to udokumentowane wymaganie. Po drugie, Apple ostrzega przed przekazywaniem pracy eksportowej z procedur obsługi reprezentacji na głównego aktora lub główny wątek, ponieważ może to spowodować zawieszenie. Eksport powinien być niezależny od mutacji interfejsu; zaktualizuj stan kolekcji po zakończeniu importu. onDrop(of:isTargeted:perform:), onDrag(_:).
W asynchronicznej implementacji dostawcy rozróżnij akceptację rozpoczęcia ładowania od zakończenia transakcji modelu. Zachowaj kolejność dostawców, przechowując wyniki pod indeksami, zgłaszaj błędy ładowania lub dekodowania i pozostaw kolekcję bez zmian, jeśli kontrakt wymaga powodzenia dla każdego elementu. Unikaj cichego odrzucania nieudanych elementów za pomocą operacji filtrowania. To reguły implementacji wynikające z polityki akceptacji partii przyjętej w przykładzie.
Zachowaj obsługę zdarzeń AppKit i semantykę przenoszenia
Jeśli istniejący widok AppKit już obsługuje interakcję myszy, pozostaw inicjowanie sesji po jego stronie. Metoda Apple beginDraggingSession(with:event:source:) przyjmuje początkowe zdarzenie myszy, elementy przeciągania, których ramki używają współrzędnych tego widoku, oraz źródło zgodne z NSDraggingSource. Wywołania zwrotne źródła obsługują rozpoczęcie sesji, ruch oraz jej ukończenie lub anulowanie. beginDraggingSession(with:event:source:).
Granica reprezentacji AppKit również jest jawna: NSDraggingItem(pasteboardWriter:) wymaga obiektu NSPasteboardWriting. Wywołanie zwrotne dostawcy SwiftUI i obiekt zapisujący na schowku AppKit to różne interfejsy. Przy integrowaniu niestandardowego widoku AppKit ze SwiftUI pozostaw obsługę zdarzeń i sesji po stronie widoku AppKit, a zaakceptowane zmiany kolekcji powierz wspólnemu modelowi. init(pasteboardWriter:).
Przykład wykonuje kopie. Jeśli zadanie zmieni się na przenoszenie istniejących elementów między kolekcjami, dodaj stabilny identyfikator elementu i określ dokładnie jednego właściciela jego usunięcia. Wspólny magazyn może przenieść element jeden raz albo miejsce docelowe może go wstawić, po czym źródło usuwa go po potwierdzeniu zakończenia przenoszenia. Połączenie obu strategii grozi podwójnym usunięciem elementu.
Aktualna dokumentacja Apple obejmuje DragConfiguration, typowane przeciążenie miejsca docelowego oparte na sesji oraz obserwowanie sesji przeciągania w macOS 26. Źródło proponuje obsługiwane operacje, miejsce docelowe wybiera operację, a zakończenie sesji pozwala źródłu zareagować na wynik. Przed zastosowaniem tej ścieżki sprawdź dostępność każdego symbolu w SDK. DragConfiguration, dropDestination(for:isEnabled:action:), onDragSessionUpdated(_:).
Nie usuwaj elementu źródłowego, gdy wskaźnik wejdzie do miejsca docelowego ani gdy pojawi się podgląd. Te zdarzenia zachodzą przed akceptacją. Przy przenoszeniu wewnątrz aplikacji istotnym warunkiem jest to, że po zakończeniu transakcji jeden logiczny element należy do jednej kolekcji.
Sprawdź kontrakt na podstawie widocznych rezultatów
Wykonaj na Macu następujące kroki, aby sprawdzić implementację:
- Przeciągnij
CircledoCollection: miejsce docelowe zyska jedno koło, a paleta zachowa swój element. - Powtórz przeciągnięcie: miejsce docelowe zyska drugi element z odrębnym identyfikatorem.
- Przeciągnij
Squarepoza miejsce docelowe i zwolnij przycisk: kolekcja się nie zmieni. - Tymczasowo zmień wersję ładunku źródłowego na
2: miejsce docelowe zgłosi odrzucenie i niczego nie doda; później przywróć1. - W implementacji opartej na dostawcy celowo zwróć błąd kodowania lub ładowania: pokaż błąd bez wstawiania niepełnej partii.
- W implementacji przenoszenia anuluj przeciąganie i sprawdź, czy pierwotny element nadal należy do swojej pierwotnej kolekcji.
Do tego zadania kopiowania między dwoma panelami wybierz typowane API SwiftUI, chyba że konkretne wymaganie zgodności lub integracji przemawia za dostawcami albo AppKit. Trwałym elementem jest kontrakt: określona wartość przekracza granicę, miejsce docelowe ją weryfikuje, a jeden właściciel modelu zatwierdza zaakceptowaną zmianę.
Źródła
Przeciąganie i upuszczanie — Apple.
Transferable — Apple.
visibility(_:) — Apple.
ownProcess — Apple.
Sendable — Apple.
onDrag(_:) — Apple.
onDrop(of:isTargeted:perform:) — Apple.
registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:) — Apple.
loadDataRepresentation(forTypeIdentifier:completionHandler:) — Apple.
init(pasteboardWriter:) — Apple.
DragConfiguration — Apple.
onDragSessionUpdated(_:) — Apple.