Für eine Mac-Arbeitsfläche mit überlappenden Drop-Bereichen platzieren Sie die native Drop-Behandlung auf einem stabilen gemeinsamen Vorfahren und ermitteln das logische Ziel anhand des aktuellen Modellzustands. Prüfen Sie die Berechtigung beim Ausführen des Drops erneut; ein abgelehnter Callback ist keine dokumentierte Übergabe an eine andere Ansicht.

Dieser Artikel zeigt, wie sich diese Interaktion für eine App mit synthetischen Formbeschriftungen und zwei überlappenden Panels umsetzen lässt. Die konkrete Aufgabe besteht darin, einem unteren Panel den Empfang eines Drops zu ermöglichen, wenn das obere Panel keine Elemente mehr annimmt – auch dann, wenn sich dessen Berechtigung während eines Ziehvorgangs ändert. Die folgende Routing-Richtlinie ist ein Anwendungsdesign, das sich aus den dokumentierten Callbacks ableitet.

Warum wird der Ziehvorgang durch eine Zustandsänderung nicht zuverlässig an ein anderes Ziel übergeben?

Unterscheiden Sie zunächst drei Fragen: Welche native Ansicht empfängt den Ziehvorgang? Welches logische Panel betrachtet Ihre App als berechtigt? Und welches Modell empfängt letztlich das Element? Eine Zustandsänderung kann Ihre Antwort auf die zweite Frage ändern, ohne eine neue Antwort auf die erste festzulegen.

Apple beschreibt SwiftUI-validateDrop(info:) als Validierung, wenn ein kompatibler Drop in ein Ziel eintritt. dropUpdated(info:) wird als Aktualisierung beschrieben, wenn ein validierter Drop innerhalb der geänderten Ansicht bewegt wird. Keine der beiden Beschreibungen verspricht eine erneute Zielsuche, sobald sich ein anderer Zustand der übergeordneten Ansicht ändert. SwiftUI-Validierung und Drop-Aktualisierungen.

Die AppKit-Grenze ist eindeutiger: Pro Sequenz empfängt jeweils nur ein Ziel draggingUpdated(_:)-Nachrichten. Sind zwei überlappende Ansichten gültige Ziele, erhält die oberste diese Nachrichten, bis das Bild losgelassen oder aus ihr herausgezogen wird. Das beschreibt das AppKit-Routing; daraus folgt kein allgemeiner SwiftUI-Vertrag zur Priorität von Geschwisteransichten. Aktualisierungen von AppKit-Drop-Zielen.

Mit false aus der älteren SwiftUI-Drop-Aktion wird gemeldet, dass der Drop fehlgeschlagen ist. Apple beschreibt diesen Rückgabewert nicht als Aufforderung, ein darunterliegendes Ziel erneut zu versuchen. Ebenso bedeutet die Rückgabe von nil aus dropUpdated(info:), dass der letzte gültige Vorschlag beibehalten oder standardmäßig kopiert wird; sie bedeutet nicht „durchreichen“. Closure-basierte Drop-Behandlung und Verhalten von Vorschlägen.

Die praktische Konsequenz ist, den Fallback als explizite Anwendungsregel festzulegen. Für eine einzelne Arbeitsfläche kann ein nativer Empfänger zwischen mehreren logischen Panels wählen, ohne sich auf eine Übergabe zwischen überlappenden nativen Empfängern in untergeordneten Ansichten zu verlassen.

Legen Sie Besitz und Fallback fest, bevor Sie Modifier hinzufügen

In diesem Beispiel liegt das orange Panel hinter dem blauen Panel. Orange nimmt die Beispiellabel immer an. Blau nimmt sie nur an, solange blueEnabled wahr ist. Das gewünschte Verhalten ist:

Position des Zeigers Blau nimmt Drops an Logischer Empfänger
Bereich nur in Orange Beliebiger Wert Orange
Überlappung Ja Blau
Überlappung Nein Orange
Bereich nur in Blau Nein Keiner
Außerhalb beider Panels Beliebiger Wert Keiner

Diese Tabelle dient als Entscheidungshilfe für die Implementierung. Insbesondere macht das Deaktivieren von Blau nicht jeden Punkt innerhalb seines Rechtecks zu einem Drop-Ziel in Orange. Orange empfängt das Element nur, wenn sich der Zeiger ebenfalls innerhalb des orangefarbenen Rechtecks befindet.

Legen Sie den für das Routing maßgeblichen Zustand oberhalb der visuellen Panels ab. Apple empfiehlt, einen Zustandswert in der höchsten Ansicht zu platzieren, die ihn benötigt, und ihn mit den untergeordneten Ansichten zu teilen. Hier besitzt ein übergeordnetes Element ein Modell mit der Berechtigung, Geometrie und empfangenen Elementen. Die untergeordneten Rechtecke bestimmen das Aussehen und erhalten keine eigenen Drop-Handler. Besitz von SwiftUI-Zustand.

Lesen Sie dieses Modell beim Ausführen der Drop-Aktion aus. Speichern Sie keine Berechtigungs-Boolean-Variable oder Zielkennung beim Eintritt und behandeln Sie sie später als maßgeblich. Swift-Closures können Werte und Referenzen aus ihrem Erzeugungskontext beibehalten; eine gespeicherte Referenz kann nach dem Ersetzen eines Besitzers auch weiterhin auf ein älteres Modell zeigen. Prüfen Sie, welches Objekt ein Callback tatsächlich erfasst. Erfassung in Swift-Closures.

Legen Sie für den Austausch des Besitzers eine eigene Richtlinie fest: Entweder wird anhand des neuen Besitzers entschieden oder Arbeit des vorherigen Besitzers wird abgelehnt. Für die Ablehnung ist es sinnvoll, dem Besitzer eine Revisionsnummer zuzuweisen, sie beim Austausch zu erhöhen und die erfasste Revision vor dem Ausführen verzögerter Arbeit zu vergleichen. Ein vorübergehender Wert wie „derzeit hervorgehoben“ sollte nicht über den Besitz entscheiden.

Erstellen Sie eine stabile SwiftUI-Drop-Fläche

Das folgende vollständige Beispiel setzt eine macOS-SwiftUI-App mit einem macOS-26-Bereitstellungsziel und einem SDK voraus, das dropDestination(for:isEnabled:action:) enthält. Apple beschreibt diese Überladung als eine API, die einen Transferable-Typ, ein Aktivierungsflag und eine Aktion mit dekodierten Elementen sowie einer DropSession entgegennimmt. Das Ziel nimmt Größe und Position der geänderten Ansicht ein. Aktuelle Drop-Ziel-API.

Erstellen Sie mit SwiftUI ein neues macOS-App-Projekt. Ersetzen Sie die generierte App-Deklaration durch die beiden folgenden Codeblöcke, die direkt nacheinander in einer Swift-Datei stehen. Zusammen bilden sie ein Programm. Es verwendet nur Zeichenfolgen und Rechtecke im Arbeitsspeicher. Die Quelllabels bleiben nach einem Drop verfügbar, daher verhält sich das Beispiel wie eine Kopieraktion.

Modell und deterministisches Routing

Das Modell prüft Blau zuerst, um die gewählte visuelle Priorität auszudrücken. Sowohl die Darstellung als auch das Routing verwenden dieselben Rechtecke. Die Commit-Methode ermittelt den Empfänger anhand der aktuellen Berechtigung erneut und validiert den gesamten Beispielstapel, bevor sie etwas hinzufügt.

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)."
    }
}

App und Drop-Empfänger

Das Rechteck des übergeordneten Elements besitzt den einzigen Drop-Modifier. Keines der farbigen Panels registriert ein konkurrierendes Ziel. Die DropSession stellt eine Position im lokalen Koordinatenraum des Ziels sowie ein localSession bereit, das einen in der App gestarteten Ziehvorgang kennzeichnet. Das Modell prüft auch die Beispielwerte. Informationen zur Drop-Session.

@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)
    }
}

SwiftUIs draggable(_:) installiert die Gesten, die benötigt werden, um einen Ziehvorgang durch Benutzereingaben zu starten. Die verzögerte Schaltfläche ändert lediglich den Berechtigungszustand des Beispiels; sie startet oder überträgt keine Ziehsitzung und setzt sie auch nicht neu auf. SwiftUI-Verhalten von Ziehquellen.

Drücken Sie auf Toggle blue in 3 seconds, ziehen Sie dann Circle in die Überlappung und halten Sie es dort, bis Blau ausgeblendet wird. Lassen Sie innerhalb der Überlappung los: Der vorgesehene Empfänger ist Orange. Lassen Sie bei deaktiviertem Blau im nicht überlappenden Bereich von Blau los: Keines der Element-Arrays ändert sich, und der Status erklärt den Grund.

Dieser Empfänger bleibt für die gesamte Arbeitsfläche aktiviert, damit er die vollständige Routing-Richtlinie anwenden kann. Daher bietet das Beispiel keinen standortspezifischen nativen Ablehnungscursor und keine Hover-Hervorhebung. Der Status meldet die Annahme durch die App nach dem Loslassen. Fügen Sie Vorschlags- und Hover-Behandlung separat hinzu, falls das Produkt diese Rückmeldung benötigt.

Der Code wurde anhand der abgerufenen API-Deklarationen geprüft. Er wurde hier nicht kompiliert oder ausgeführt, da die verfügbare Umgebung weder eine Swift-Toolchain noch eine macOS-Laufzeitumgebung enthält.

Behandeln Sie Hover-Aktualisierungen und ältere Bereitstellungsziele separat

Wenn Sie Systeme ohne die neuere Überladung unterstützen, behalten Sie dieselbe Modell- und Geometrierichtlinie bei und wählen eine verfügbare Empfangs-API. Das ältere dropDestination(for:action:isTargeted:) übergibt dekodierte Elemente und einen lokalen Punkt; sein Boolean-Rückgabewert meldet den Erfolg. Sein isTargeted-Closure beschreibt den Eintritt in und den Austritt aus dem Bereich des nativen Ziels, der in diesem Design die gesamte Arbeitsfläche umfasst. Es identifiziert nicht das logische Panel unter dem Zeiger. Vertrag des älteren Drop-Ziels.

Für eine explizite Validierung und Aktionsvorschläge installiert onDrop(of:delegate:) einen DropDelegate. Verwenden Sie validateDrop(info:) für die anfängliche Berechtigung, dropUpdated(info:) für aktuelle Vorschläge und performDrop(info:) für die abschließende Validierung und Übernahme. .forbidden zurückzugeben, beschreibt eine an dieser Position unzulässige Aktion; eine Übergabe an einen anderen nativen Empfänger wird dadurch nicht dokumentiert. Delegate-basierte Ziele und Drop-Aktionen.

Bei Verwendung von Providern gibt es eine wichtige Grenze für das Laden: DropInfo.itemProviders(for:) ist nur während performDrop gültig. Starten Sie das Laden der Provider-Inhalte, bevor diese Methode zurückkehrt. Der Abschluss kann später erfolgen. Prüfen Sie daher die Identität des Ziels, seine Revision und die weiterhin geltende Berechtigung erneut, bevor Sie das Modell ändern. Melden Sie einen abgelehnten oder fehlgeschlagenen Ladevorgang, anstatt das Ergebnis stillschweigend zu verwerfen. Zugriff auf Provider und Anforderungen an den Abschluss eines Drops.

Speichern Sie für die Hover-Darstellung vorübergehende Sitzungsinformationen getrennt von den empfangenen Elementen. Die neuere Dokumentation zu DropSession verweist auf onDropSessionUpdated(_:), um Phasenänderungen zu beobachten. Gehen Sie nicht davon aus, dass eine Modellzustandsänderung eine weitere Zeigeraktualisierung auslöst. Wenn Sie bereits eine gültige Hover-Position haben, berechnen Sie die logische Hervorhebung auch dann neu, wenn sich die Berechtigung ändert. Rufen Sie die Routing-Funktion für die endgültige Annahme dennoch erneut auf. Lebenszyklus der Drop-Session.

Wann sollte die Arbeitsfläche stattdessen AppKit verwenden?

Verwenden Sie ein AppKit-Ziel, wenn die erforderliche Interaktion dokumentierte regelmäßige native Aktualisierungen voraussetzt, während der Zeiger stillsteht. AppKits wantsPeriodicDraggingUpdates() steuert dieses Verhalten: Gibt die Methode false zurück, erfolgen Aktualisierungen nur bei Mausbewegungen oder Änderungen von Zusatztasten; andernfalls werden sie auch dann regelmäßig ausgeführt, wenn sich nichts ändert. So kann die aktuelle Aktion während eines stationären Ziehvorgangs auf dokumentierte Weise erneut ausgewertet werden. Regelmäßige Aktualisierungen beim Ziehen.

Auch hier besteht die passende Architektur aus einem einzigen Ziel für die Arbeitsfläche. Registrieren Sie die akzeptierten Pasteboard-Typen, ermitteln Sie logische Panels während draggingEntered(_:) und draggingUpdated(_:), prüfen Sie die Annahme beim Loslassen und übernehmen Sie das Element in performDragOperation(_:). Apple beschreibt ausdrücklich, wie sich eine Aktion für Grafik- oder Textbereiche anpassen lässt, die sich innerhalb einer einzelnen Zielansicht befinden. Lebenszyklus eines AppKit-Ziels und positionsabhängige Aktionen.

Wenn Sie dieses Ziel mit NSViewRepresentable einbetten, aktualisieren Sie seine Modellreferenz, die Berechtigungskonfiguration und die Callbacks in updateNSView(_:context:). Die anfängliche Erstellung allein kann spätere Änderungen des Besitzers nicht synchronisieren. Apple nennt die Aktualisierungsmethode als den Ort, an dem die AppKit-Ansicht mit dem SwiftUI-Zustand in Einklang gebracht wird. Aktualisieren einer AppKit-Ansicht.

Behalten Sie auch die Grenze für das Quellereignis bei. AppKits beginDraggingSession(with:event:source:) startet eine Sitzung mit ziehbaren Elementen und dem ursprünglichen Maus-Down-Ereignis. Es ist keine API zur Neuzuordnung eines Drop-Ziels. Behalten Sie eine vorhandene SwiftUI-Quelle bei, wenn sie die Aufgabe erfüllt; verwenden Sie eine AppKit-Quelle nur, wenn Sie auch diese quellseitige Interaktion selbst steuern müssen. Starten eines AppKit-Ziehvorgangs.

Prüfen Sie Zustandsänderungen an den Punkten, die den Empfänger bestimmen

Führen Sie die folgenden manuellen Prüfungen auf den von Ihnen unterstützten macOS-Bereitstellungsversionen durch. Damit lässt sich korrektes Routing von einer visuellen Änderung unterscheiden, die nur scheinbar funktioniert:

  • Lassen Sie bei aktiviertem Blau in der Überlappung los: Nur das Array von Blau erhält ein Element.
  • Deaktivieren Sie Blau vor dem Ziehvorgang: Die Überlappung wird an Orange geleitet.
  • Deaktivieren Sie Blau während eines stationären Ziehvorgangs: Bei der endgültigen Annahme wird der deaktivierte Zustand verwendet.
  • Lassen Sie bei deaktiviertem Blau im Bereich los, der nur zu Blau gehört: Beide Arrays bleiben unverändert und der Status erklärt das Ergebnis.
  • Aktivieren Sie Blau während des Ziehvorgangs erneut: Beim Ausführen wird die Überlappung an Blau geleitet.
  • Brechen Sie einen Ziehvorgang ab: Keines der Arrays ändert sich.
  • Wenn Ihre App die Besitzer der Arbeitsfläche austauscht, tauschen Sie einen während eines Ziehvorgangs aus und prüfen Sie die gewählte Richtlinie für den Besitzerwechsel.
  • Wenn Ihre Implementierung Provider asynchron lädt, ändern Sie den Besitzer vor dem Abschluss und prüfen Sie die Revisionskontrollen sowie die sichtbare Fehlermeldung.

Erweitern Sie bei einer scrollbaren, zoombaren oder in der Größe veränderbaren Arbeitsfläche dasselbe Design, indem Sie die Grenzen jedes Panels und die Drop-Position in einen gemeinsamen Koordinatenraum umrechnen. Die festen Rechtecke in diesem Beispiel machen diese Umrechnung überflüssig; ihre Zahlen unverändert in ein transformiertes Layout zu übernehmen, würde das Routing beschädigen.

Ein stabiler nativer Empfänger und eine Routing-Funktion, die den aktuellen Zustand verwendet, sorgen für ein eindeutiges Ergebnis: Das oberste berechtigte Panel erhält das Element, ein berechtigtes unteres Panel erhält den Fallback für die Überlappung, und an einer nicht abgedeckten Position bleibt das Modell unverändert. Verwenden Sie dieselbe Richtlinie für die Hover-Rückmeldung und die endgültige Annahme; prüfen Sie bei der endgültigen Annahme den Zustand erneut.

Quellen