För en Mac-canvas med överlappande släppzoner placerar du den inbyggda släpphanteringen på en stabil överordnad vy och avgör den logiska destinationen utifrån det aktuella modelltillståndet. Kontrollera behörigheten på nytt när släppet genomförs; ett avvisat anrop innebär inte enligt dokumentationen att hanteringen lämnas över till en annan vy.
Den här artikeln bygger upp interaktionen för en app med syntetiska formetiketter och två överlappande paneler. Den konkreta uppgiften är att låta en underliggande panel ta emot ett släpp när den övre panelen slutar acceptera objekt, även om dess behörighet ändras under en dragning. Routningspolicyn nedan är en applikationsdesign som härletts från de dokumenterade anropen.
Varför överlämnas inte dragningen tillförlitligt till ett annat mål när tillståndet ändras?
Börja med att skilja mellan tre frågor: vilken inbyggd vy som tar emot dragningen, vilken logisk panel appen anser vara behörig och vilken modell som tar emot det slutliga objektet. En tillståndsändring kan ändra svaret på den andra frågan utan att fastställa ett nytt svar på den första.
Apple dokumenterar SwiftUI:s validateDrop(info:) som validering när en kompatibel släppning kommer in i en destination. dropUpdated(info:) dokumenteras som en uppdatering när ett validerat släpp flyttas inom den modifierade vyn. Ingen av beskrivningarna utlovar en ny destinationssökning varje gång orelaterat tillstånd som ägs av vyn ändras. SwiftUI-validering och släppuppdateringar.
Gränsen mot AppKit är tydligare: endast en destination tar emot en sekvens av meddelanden av typen draggingUpdated(_:) åt gången. När två överlappande vyer är giltiga destinationer tar den översta emot meddelandena tills bilden släpps eller dras utanför. Det beskriver AppKit-routning; det fastställer inte något allmänt avtal om prioritet mellan SwiftUI-syskonvyer. AppKit-destinationsuppdateringar.
Om den äldre släppåtgärden i SwiftUI returnerar false rapporteras att släppet misslyckades. Apple beskriver inte returvärdet som en begäran om att försöka med en underliggande destination. På samma sätt behåller nil från dropUpdated(info:) det senaste giltiga förslaget eller använder kopiering som standard; det betyder inte ”släpp igenom”. Släpphantering med closure och förslagsbeteende.
Den praktiska följden är att göra reservhanteringen till en uttrycklig applikationsregel. För en enda canvas kan en inbyggd mottagare välja mellan flera logiska paneler utan att förlita sig på att inbyggd hantering lämnas över mellan överlappande underordnade mottagare.
Definiera ägarskap och reservhantering innan du lägger till modifierare
I det här exemplet ligger den orange panelen bakom den blå. Orange accepterar alltid exempel etiketterna. Blå accepterar dem bara när blueEnabled är sant. Det avsedda beteendet är:
| Pekarens position | Blå accepterar släpp | Logisk mottagare |
|---|---|---|
| Område som bara täcks av orange | Båda värdena | Orange |
| Överlappning | Ja | Blå |
| Överlappning | Nej | Orange |
| Område som bara täcks av blå | Nej | Ingen |
| Utanför båda panelerna | Båda värdena | Ingen |
Tabellen vägleder implementeringen. Att inaktivera blå gör i synnerhet inte varje punkt inom dess rektangel till en destination för orange. Orange tar bara emot objektet när pekaren också ligger inom den orange rektangeln.
Håll tillståndet som styr routningen ovanför de visuella panelerna. Apple rekommenderar att ett tillståndsvärde placeras i den högsta vy som behöver det och delas med underordnade vyer. Här äger en enda överordnad vy en modell med behörighet, geometri och mottagna objekt. De underordnade rektanglarna står för utseendet i stället för att ha egna släpphanterare. Tillståndsägarskap i SwiftUI.
Läs modellen när släppåtgärden körs. Undvik att spara ett behörighetsvärde eller ett destinations-ID vid inträdet och senare behandla det som auktoritativt. Swift-closures kan behålla värden och referenser från det sammanhang där de skapades; en behållen referens kan också fortsätta peka på en äldre modell efter att en ägare ersatts. Granska vilket objekt ett anrop faktiskt fångar. Swift-closure-fångst.
Välj en separat policy för ägarbyte: antingen avgör du destinationen utifrån den nya ägaren eller avvisar arbete som hör till den tidigare ägaren. En användbar lösning för avvisning kopplar ett revisionsnummer till ägaren, ökar det vid ersättning och jämför det fångade revisionsnumret innan fördröjt arbete genomförs. Ett tillfälligt värde för ”markerad just nu” bör inte avgöra ägarskapet.
Bygg en stabil släppyta i SwiftUI
Följande fullständiga exempel kräver en macOS-app med SwiftUI, macOS 26 som lägsta distributionsversion och en SDK som innehåller dropDestination(for:isEnabled:action:). Apple dokumenterar den överlagringen som en variant som tar emot en Transferable-typ, en aktiveringsflagga och en åtgärd som får avkodade objekt och en DropSession. Destinationen upptar den modifierade vyns storlek och position. Aktuellt API för släppdestinationer.
Skapa ett nytt macOS App-projekt med SwiftUI. Ersätt den genererade appdeklarationen med de två kodblocken nedan, placerade efter varandra i en och samma Swift-fil. Tillsammans bildar de ett program. Det använder bara strängar och rektanglar i minnet. Källetiketterna finns kvar efter ett släpp, så exemplet fungerar som en kopieringsåtgärd.
Modell och deterministisk routning
Modellen kontrollerar blå först för att uttrycka den valda visuella prioriteten. Både rendering och routning använder samma rektanglar. Dess genomförandemetod beräknar mottagaren på nytt utifrån aktuell behörighet och validerar hela exempelbatchen innan något läggs till.
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 och släppmottagare
Den överordnade rektangeln äger den enda släppmodifieraren. Ingen av de färgade panelerna registrerar någon konkurrerande destination. DropSession tillhandahåller en position i destinationens lokala koordinatsystem och en localSession som identifierar en dragning som startats i appen. Modellen kontrollerar också exempelvärdena. Information om släppsessionen.
@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)
}
}
SwiftUI:s draggable(_:) installerar de gester som behövs för att starta dragningen via användarinmatning. Den fördröjda knappen ändrar bara exempel appens behörighetstillstånd; den startar, startar om eller överför inte en dragning. Dragkällans beteende i SwiftUI.
Tryck på Toggle blue in 3 seconds, dra sedan Circle till överlappningen och håll kvar den där tills blå tonas bort. Släpp inom överlappningen: den avsedda mottagaren är orange. Om blå är inaktiverad och du släpper i dess område utanför överlappningen ändras ingen av objektlistorna, och statusen förklarar varför.
Mottagaren är fortsatt aktiverad över hela canvasen så att den kan tillämpa hela routningspolicyn. Därför tillhandahåller exemplet ingen platsspecifik inbyggd avvisningsmarkör eller hovringsmarkering. Statusen rapporterar om appen accepterar objektet efter släppet. Lägg till hantering av förslag och hovring separat om produkten behöver sådan återkoppling.
Koden granskades mot de API-deklarationer som hämtades. Den kompilerades eller kördes inte här eftersom den tillgängliga miljön saknar Swift-verktygskedja och macOS-körtidsmiljö.
Hantera hovringsuppdateringar och äldre distributionsmål separat
Om du stöder system utan den nyare överlagringen behåller du samma modell- och geometripolicy, men väljer ett tillgängligt mottagar-API. Den äldre dropDestination(for:action:isTargeted:) skickar avkodade objekt och en lokal punkt; dess Bool-resultat anger om åtgärden lyckades. Dess isTargeted-closure beskriver när pekaren går in i och lämnar den inbyggda destinationens område, som i den här designen är hela canvasen. Den identifierar inte den logiska panelen under pekaren. Avtalet för äldre släppdestinationer.
För uttrycklig validering och åtgärdsförslag installerar onDrop(of:delegate:) en DropDelegate. Använd validateDrop(info:) för initial behörighet, dropUpdated(info:) för aktuella förslag och performDrop(info:) för slutlig validering och införlivande. Returvärdet .forbidden beskriver en otillåten åtgärd på den platsen; det dokumenteras inte som en överlämning till en annan inbyggd mottagare. Delegatbaserade destinationer och släppåtgärder.
När providrar används finns en viktig gräns för inläsning: DropInfo.itemProviders(for:) är bara giltig under performDrop. Börja läsa in providerinnehåll innan den metoden returnerar. Slutförandet kan komma senare, så kontrollera destinationens identitet, revision och fortsatta behörighet innan modellen ändras. Rapportera ett avvisat eller misslyckat anrop i stället för att tyst kasta bort resultatet. Provideråtkomst och krav för släppslutförande.
För hovringspresentation lagrar du tillfällig sessionsinformation separat från mottagna objekt. Den nyare dokumentationen för DropSession hänvisar till onDropSessionUpdated(_:) för att observera fasändringar. Utgå inte från att en ändring av modellens tillstånd skapar en ny pekaruppdatering; om du redan har en giltig hovringsposition ska du också beräkna den logiska markeringen på nytt när behörigheten ändras. Den slutliga acceptansen bör fortfarande anropa routningsfunktionen igen. Släppsessionens livscykel.
När bör canvasen använda AppKit i stället?
Använd en AppKit-destination när den interaktion som krävs är beroende av dokumenterade, återkommande inbyggda uppdateringar medan pekaren står stilla. AppKit-funktionen wantsPeriodicDraggingUpdates() styr detta beteende: om false returneras begränsas uppdateringarna till musrörelser eller ändringar av modifierare; annars sker periodiska uppdateringar även när inget ändras. Det ger ett dokumenterat sätt att omvärdera den aktuella åtgärden under en stillastående dragning. Periodiska draguppdateringar.
Motsvarande arkitektur använder fortfarande en enda destination för canvasen. Registrera dess accepterade pasteboard-typer, avgör logiska paneler under draggingEntered(_:) och draggingUpdated(_:), kontrollera acceptansen vid släpp och införliva objektet i performDragOperation(_:). Apple beskriver uttryckligen hur en åtgärd kan anpassas efter grafik- eller textområden inom en enda destinationsvy. AppKit-destinationens livscykel och positionsberoende åtgärder.
När destinationen bäddas in med NSViewRepresentable uppdaterar du modellreferensen, behörighetskonfigurationen och anropen i updateNSView(_:context:). Att bara skapa vyn initialt kan inte synkronisera senare ägarändringar; Apple anger uppdateringsmetoden som platsen där AppKit-vyn ska anpassas efter SwiftUI-tillståndet. Uppdatera en AppKit-vy.
Bevara även källans händelsegräns. AppKit-funktionen beginDraggingSession(with:event:source:) startar en session utifrån objekt som dras och den ursprungliga musnedtryckningshändelsen. Det är inte ett API för att byta släppdestination. Behåll en befintlig SwiftUI-källa om den uppfyller uppgiften; använd en AppKit-källa bara om du också behöver styra interaktionen på källsidan. Starta en AppKit-dragning.
Kontrollera tillståndsändringar vid de tidpunkter som påverkar ägarskapet
Gör följande manuella kontroller på de Mac-versioner du stöder. De skiljer korrekt routning från en visuell ändring som bara verkar fungera:
- Släpp i överlappningen när blå är aktiverad: endast blå får ett objekt i sin lista.
- Inaktivera blå före dragningen: överlappningen routas till orange.
- Inaktivera blå under en stillastående dragning: den slutliga acceptansen använder det inaktiverade tillståndet.
- Släpp i området som bara täcks av blå när blå är inaktiverad: båda listorna förblir oförändrade och statusen förklarar resultatet.
- Aktivera blå igen under dragningen: överlappningen routas till blå när släppet genomförs.
- Avbryt en dragning: ingen av listorna ändras.
- Om appen byter ägare för canvasen byter du ägare under en dragning och kontrollerar den valda policyn för ägarbyte.
- Om implementeringen läser in providerdata asynkront byter du ägare innan inläsningen är klar och kontrollerar revisionskontrollerna och den synliga felrapporteringen.
För en canvas som kan rullas, zoomas eller ändra storlek utökar du samma design genom att konvertera varje panels gränser och släppositionen till ett gemensamt koordinatsystem. De fasta rektanglarna i det här exemplet gör den konverteringen onödig; om du kopierar deras siffror till en transformerad layout bryts routningspolicyn.
En stabil inbyggd mottagare och en routningsfunktion som använder aktuellt tillstånd ger interaktionen ett uttryckligt resultat: den översta behöriga panelen tar emot objektet, en behörig underliggande panel tar emot det som reserv vid överlappning och en position utan täckning lämnar modellen oförändrad. Använd samma policy för hovringsåterkoppling och slutlig acceptans, och låt den slutliga acceptansen göra en egen kontroll.
Källor
validateDrop(info:) — Apple.
dropUpdated(info:) — Apple.
draggingUpdated(_:) — Apple.
Tillstånd — Apple.
Closures — 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.