Pour un canevas Mac avec des zones de dépôt qui se chevauchent, placez la gestion native du dépôt sur un seul ancêtre stable et déterminez la destination logique à partir de l’état actuel du modèle. Vérifiez à nouveau l’admissibilité au moment de valider le dépôt ; un rappel rejeté ne constitue pas, selon la documentation, un transfert vers une autre vue.
Cet article met en place cette interaction pour une app contenant des étiquettes de formes synthétiques et deux panneaux qui se chevauchent. L’objectif concret consiste à permettre à un panneau inférieur de recevoir un dépôt lorsque le panneau supérieur cesse d’accepter des éléments, y compris lorsque son admissibilité change pendant un glisser-déposer. La politique de routage ci-dessous est une conception d’application fondée sur les rappels documentés.
Pourquoi un changement d’état ne transfère-t-il pas le glisser-déposer de façon fiable vers une autre cible ?
Commencez par distinguer trois questions : quelle vue native reçoit le glisser-déposer, quel panneau logique votre app considère comme admissible et quel modèle reçoit l’élément final. Un changement d’état peut modifier votre réponse à la deuxième question sans en établir une nouvelle pour la première.
Apple décrit validateDrop(info:) de SwiftUI comme une validation qui s’effectue lorsqu’un dépôt compatible entre dans une destination. La documentation décrit dropUpdated(info:) comme une mise à jour lorsqu’un dépôt validé se déplace à l’intérieur de la vue modifiée. Aucune de ces descriptions ne garantit une nouvelle recherche de destination à chaque changement de l’état d’une vue propriétaire sans rapport avec le dépôt. Validation SwiftUI et mises à jour de dépôt.
La frontière AppKit est plus explicite : une seule destination reçoit une séquence de messages draggingUpdated(_:) à la fois. Lorsque deux vues qui se chevauchent sont des destinations valides, celle qui se trouve le plus en avant reçoit ces messages jusqu’à ce que l’image soit relâchée ou déplacée hors de la vue. Cela décrit le routage AppKit ; cela n’établit pas de règle générale de priorité entre vues sœurs dans SwiftUI. Mises à jour des destinations AppKit.
Renvoyer false depuis l’ancienne action de dépôt SwiftUI indique que le dépôt a échoué. Apple ne décrit pas cette valeur de retour comme une demande visant à réessayer le dépôt sur une destination sous-jacente. De même, renvoyer nil depuis dropUpdated(info:) conserve la dernière proposition valide ou choisit la copie par défaut ; cela ne signifie pas « laisser passer ». Gestion des dépôts par fermeture et comportement des propositions.
En pratique, il faut donc définir explicitement la règle de repli dans l’application. Pour un canevas unique, un seul récepteur natif peut choisir parmi plusieurs panneaux logiques sans dépendre d’un transfert natif entre des récepteurs enfants qui se chevauchent.
Définir la propriété et le comportement de repli avant d’ajouter les modificateurs
Dans cet exemple, le panneau orange se trouve derrière le panneau bleu. L’orange accepte toujours les étiquettes d’exemple. Le bleu les accepte uniquement lorsque blueEnabled est vrai. Le comportement souhaité est le suivant :
| Emplacement du pointeur | Le bleu accepte les dépôts | Destinataire logique |
|---|---|---|
| Zone propre à l’orange | Quelle que soit la valeur | Orange |
| Chevauchement | Oui | Bleu |
| Chevauchement | Non | Orange |
| Zone propre au bleu | Non | Aucun |
| À l’extérieur des deux panneaux | Quelle que soit la valeur | Aucun |
Ce tableau sert de guide de décision pour l’implémentation. En particulier, désactiver le bleu ne fait pas de chaque point à l’intérieur de son rectangle une destination orange. L’orange ne reçoit l’élément que si le pointeur se trouve aussi à l’intérieur de son rectangle.
Conservez au-dessus des panneaux visuels l’état qui détermine le routage. Apple recommande de placer une valeur d’état dans la vue la plus haute qui en a besoin et de la partager avec ses descendants. Ici, un parent possède un modèle qui contient l’admissibilité, la géométrie et les éléments reçus. Les rectangles enfants définissent l’apparence et n’ont pas de gestionnaires de dépôt indépendants. Propriété de l’état dans SwiftUI.
Lisez ce modèle au moment où l’action de dépôt s’exécute. Évitez d’enregistrer une valeur booléenne d’admissibilité ou un identifiant de destination à l’entrée, puis de le considérer ensuite comme faisant autorité. Les fermetures Swift peuvent conserver des valeurs et des références issues de leur contexte de création ; une référence conservée peut également continuer à désigner un ancien modèle après le remplacement d’un propriétaire. Vérifiez quel objet le rappel capture réellement. Capture des fermetures Swift.
Pour le remplacement du propriétaire, choisissez une politique distincte : soit déterminer la destination à partir du nouveau propriétaire, soit rejeter les opérations liées à l’ancien propriétaire. Une méthode de rejet utile consiste à associer un numéro de révision au propriétaire, à l’incrémenter lors de son remplacement, puis à comparer la révision capturée avant de valider une opération différée. Une valeur temporaire indiquant « actuellement en surbrillance » ne doit pas déterminer la propriété.
Créer une seule surface de dépôt SwiftUI stable
L’exemple complet suivant nécessite une app SwiftUI pour macOS avec une cible de déploiement macOS 26 et un SDK contenant dropDestination(for:isEnabled:action:). Apple décrit cette surcharge comme acceptant un type Transferable, un indicateur d’activation et une action qui reçoit les éléments décodés ainsi qu’un DropSession. La destination occupe la taille et la position de la vue modifiée. API actuelle de destination de dépôt.
Créez un nouveau projet d’app macOS avec SwiftUI. Remplacez la déclaration d’app générée par les deux blocs de code ci-dessous, placés l’un à la suite de l’autre dans un seul fichier Swift. Ils forment ensemble un programme complet. Il utilise uniquement des chaînes et des rectangles en mémoire. Les étiquettes source restent disponibles après un dépôt ; l’exemple se comporte donc comme une opération de copie.
Modèle et routage déterministe
Le modèle vérifie d’abord le bleu pour respecter la priorité visuelle choisie. Le rendu et le routage utilisent les mêmes rectangles. Sa méthode de validation recalcule le destinataire en fonction de l’admissibilité actuelle et valide l’ensemble des éléments d’exemple avant d’en ajouter un seul.
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 et récepteur de dépôt
Le rectangle parent possède le seul modificateur de dépôt. Aucun des panneaux colorés n’enregistre une destination concurrente. Le DropSession fournit un emplacement dans l’espace de coordonnées local de la destination et un localSession qui indique qu’un glisser-déposer provient de l’app. Le modèle vérifie également les valeurs d’exemple. Informations de session de dépôt.
@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)
}
}
draggable(_:) de SwiftUI installe les gestes nécessaires pour commencer le glisser-déposer à l’aide d’une interaction utilisateur. Le bouton différé ne modifie que l’état d’admissibilité de l’exemple ; il ne démarre, ne redémarre et ne transfère pas une session de glisser-déposer. Comportement des sources de glisser-déposer dans SwiftUI.
Appuyez sur Toggle blue in 3 seconds, puis faites glisser Circle dans la zone de chevauchement et maintenez-le là jusqu’à ce que le bleu s’estompe. Relâchez-le dans la zone de chevauchement : le destinataire prévu est l’orange. Si vous le relâchez dans la partie du bleu qui ne chevauche pas l’orange alors que le bleu est désactivé, aucun des deux tableaux d’éléments ne change et le statut en explique la raison.
Ce récepteur reste activé sur tout le canevas afin de pouvoir appliquer la politique de routage complète. L’exemple ne fournit donc ni curseur natif de rejet selon l’emplacement, ni surbrillance au survol. Son statut indique l’acceptation par l’application après le relâchement. Ajoutez séparément la gestion des propositions et du survol si le produit a besoin de ce retour visuel.
Le code a été vérifié à partir des déclarations d’API consultées. Il n’a pas été compilé ni exécuté ici, car l’environnement disponible ne dispose ni d’une chaîne d’outils Swift ni d’un environnement d’exécution macOS.
Traiter séparément les mises à jour au survol et les anciennes cibles de déploiement
Si vous prenez en charge des systèmes sans la nouvelle surcharge, conservez la même politique de modèle et de géométrie tout en choisissant une API de réception disponible. L’ancien dropDestination(for:action:isTargeted:) transmet les éléments décodés et un point local ; sa valeur booléenne de retour indique si l’opération a réussi. Sa fermeture isTargeted décrit l’entrée dans la zone de destination native et la sortie de celle-ci, qui, dans cette conception, correspond à tout le canevas. Elle n’indique pas quel panneau logique se trouve sous le pointeur. Contrat de l’ancienne destination de dépôt.
Pour une validation explicite et des propositions d’opération, onDrop(of:delegate:) installe un DropDelegate. Utilisez validateDrop(info:) pour l’admissibilité initiale, dropUpdated(info:) pour les propositions actuelles et performDrop(info:) pour la validation finale et l’incorporation. Renvoyer .forbidden indique que l’activité est interdite à cet emplacement ; cela ne documente pas un transfert vers un autre récepteur natif. Destinations avec délégué et opérations de dépôt.
Les fournisseurs imposent une limite importante : DropInfo.itemProviders(for:) n’est valide que pendant performDrop. Commencez à charger le contenu des fournisseurs avant le retour de cette méthode. L’opération peut se terminer plus tard ; vérifiez alors à nouveau l’identité de la destination, sa révision et son admissibilité avant de modifier le modèle. Signalez le rejet ou l’échec du chargement au lieu d’ignorer silencieusement le résultat. Accès aux fournisseurs et exigences de fin de dépôt.
Pour l’affichage au survol, stockez les informations temporaires de session séparément des éléments reçus. La documentation du nouveau DropSession renvoie à onDropSessionUpdated(_:) pour observer les changements de phase. Ne supposez pas qu’un changement d’état du modèle déclenche une autre mise à jour du pointeur ; si vous disposez déjà d’un emplacement de survol valide, recalculez aussi la surbrillance logique lorsque l’admissibilité change. L’acceptation finale doit tout de même rappeler la fonction de routage. Cycle de vie d’une session de dépôt.
Quand le canevas doit-il utiliser AppKit ?
Utilisez une destination AppKit lorsque l’interaction requise dépend de mises à jour natives périodiques documentées alors que le pointeur reste immobile. wantsPeriodicDraggingUpdates() d’AppKit contrôle ce comportement : renvoyer false limite les mises à jour aux mouvements de la souris ou aux changements de modificateurs ; sinon, des mises à jour périodiques ont lieu même si rien ne change. Cela fournit une méthode documentée pour réévaluer l’opération en cours pendant un glisser-déposer immobile. Mises à jour périodiques du glisser-déposer.
L’architecture correspondante utilise toujours une seule destination pour le canevas. Enregistrez ses types de presse-papiers acceptés, déterminez les panneaux logiques pendant draggingEntered(_:) et draggingUpdated(_:), vérifiez l’acceptation au relâchement et incorporez l’élément dans performDragOperation(_:). Apple décrit précisément l’adaptation d’une opération aux zones graphiques ou textuelles contenues dans une seule vue de destination. Cycle de vie d’une destination AppKit et opérations dépendant de l’emplacement.
Lorsque vous intégrez cette destination avec NSViewRepresentable, mettez à jour sa référence au modèle, sa configuration d’admissibilité et ses rappels dans updateNSView(_:context:). La création initiale ne peut pas, à elle seule, synchroniser les changements ultérieurs du propriétaire ; Apple indique que la méthode de mise à jour est l’endroit où mettre la vue AppKit en accord avec l’état SwiftUI. Mise à jour d’une vue AppKit.
Conservez également la frontière de l’événement source. beginDraggingSession(with:event:source:) d’AppKit démarre une session à partir d’éléments glissés et de l’événement de clic initial de la souris. Ce n’est pas une API de redirection de destination. Conservez une source SwiftUI existante si elle répond au besoin ; utilisez une source AppKit uniquement si vous devez également gérer cette interaction côté source. Démarrer un glisser-déposer AppKit.
Vérifier les changements d’état aux étapes qui influent sur la propriété
Effectuez les vérifications manuelles suivantes sur les versions de macOS que vous prenez en charge. Elles permettent de distinguer un routage correct d’un changement visuel qui semble simplement fonctionner :
- Effectuez un dépôt dans la zone de chevauchement avec le bleu activé : seul le tableau du bleu reçoit un élément.
- Désactivez le bleu avant de commencer le glisser-déposer : la zone de chevauchement est dirigée vers l’orange.
- Désactivez le bleu pendant un glisser-déposer immobile : l’acceptation finale utilise l’état désactivé.
- Effectuez un dépôt dans la zone propre au bleu alors qu’il est désactivé : aucun des deux tableaux ne change et le statut explique le résultat.
- Réactivez le bleu pendant le glisser-déposer : la zone de chevauchement est dirigée vers le bleu au moment de la validation.
- Annulez un glisser-déposer : aucun des deux tableaux ne change.
- Si votre app remplace les propriétaires du canevas, remplacez-en un pendant un glisser-déposer et vérifiez la politique choisie pour le remplacement du propriétaire.
- Si votre implémentation charge des fournisseurs de manière asynchrone, changez de propriétaire avant la fin du chargement et vérifiez les contrôles de révision ainsi que le signalement visible de l’échec.
Pour un canevas défilant, agrandi ou redimensionnable, étendez la même conception en convertissant les limites de chaque panneau et l’emplacement du dépôt dans un espace de coordonnées commun. Les rectangles fixes de cet exemple évitent cette conversion ; copier leurs valeurs dans une disposition transformée romprait la politique de routage.
Un récepteur natif stable et une fonction de routage fondée sur l’état actuel donnent à cette interaction un résultat explicite : le panneau admissible le plus en avant reçoit l’élément, un panneau inférieur admissible reçoit l’élément en cas de chevauchement et un emplacement non couvert laisse le modèle inchangé. Appliquez cette politique au retour visuel du survol et à l’acceptation finale ; cette dernière doit effectuer sa propre vérification.
Sources
validateDrop(info:) — Apple.
dropUpdated(info:) — Apple.
draggingUpdated(_:) — Apple.
State — 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.