Rétablissez l’apparence des formes estompées, des contours et des libellés de glissement depuis le rappel de fin de session de la source. Le rappel de sortie d’une destination signifie seulement que le pointeur a quitté cette cible. Pour une app Mac, utilisez l’API de session source de SwiftUI lorsqu’elle est disponible, ou confiez le glissement à une source AppKit.
Ce guide traite d’une seule tâche : rendre à une forme synthétique son apparence normale lorsque l’utilisateur a fini de la faire glisser, y compris lorsqu’aucune destination ne l’accepte. L’exemple conserve son contenu en mémoire et traite un dépôt accepté comme une copie ; rétablir le retour visuel ne nécessite donc jamais de supprimer ni de déplacer la source.
Pourquoi le retour visuel du glissement reste-t-il actif hors des cibles de dépôt ?
Une implémentation courante définit un indicateur isDragging au début du glissement et le réinitialise dans performDrop. Cela lie le nettoyage à un événement qu’une destination qui accepte le dépôt est la seule à pouvoir gérer. Si l’utilisateur relâche le pointeur dans une zone vide de la fenêtre, aucune destination n’est chargée de réinitialiser la source.
Apple définit DropDelegate comme une interface destinée à une vue modifiée pour accepter des dépôts. Sa méthode dropExited(info:) indique qu’un glissement validé a quitté cette vue. Le glissement peut se poursuivre vers une autre cible, revenir à la première ou se terminer ailleurs. Par conséquent, la sortie d’une cible ne permet pas d’établir que la session source est terminée. DropDelegate
La même distinction s’applique à dropDestination(for:action:isTargeted:) : isTargeted indique si le pointeur se trouve dans la zone cible. Utilisez-le pour la bordure ou l’arrière-plan de la cible. Laissez un rappel de session source contrôler l’estompage de la source ou son libellé de glissement. dropDestination(for:action:isTargeted:)
Une règle de conception utile découle de ces limites documentées : attribuez chaque état visuel au propriétaire de l’événement qui peut y mettre fin de manière fiable.
| Retour visuel ou résultat | Propriétaire | Point de réinitialisation ou de validation |
|---|---|---|
| Forme source estompée | Source du glissement | Fin de la session source |
| Destination mise en évidence | Destination du dépôt | Sortie du pointeur, avec un nettoyage défensif à la fin de la source pour un glissement local |
| Élément synthétique accepté | Destination/modèle | Le dépôt est validé et réussi |
| Ressources exportées temporaires | Cycle de vie du transfert | Fin du transfert, le cas échéant |
Quelle approche prise en charge une app Mac doit-elle utiliser ?
La documentation de référence actuelle d’Apple indique que onDragSessionUpdated(_:) est disponible à partir de macOS 26. Il observe les mises à jour de la session de glissement la plus proche dans la hiérarchie de vues enfant. Attachez-le directement à l’extérieur du modificateur de glissement de la source afin que sa portée soit claire. Utilisez un SDK qui déclare effectivement la méthode ; sa disponibilité à l’exécution ne permet pas de compiler du code si la déclaration manque dans le SDK. onDragSessionUpdated(_:)
Il existe une divergence dans la documentation qu’il convient de traiter avec prudence : l’exemple du modificateur montre une phase terminée avec des arguments de position et d’opération, tandis que la référence dédiée à la phase déclare ended(DropOperation). L’exemple ci-dessous compare la phase à .ended sans extraire les valeurs associées, car le rétablissement du retour visuel n’en a pas besoin. Consultez la déclaration du SDK installé avant d’ajouter un comportement propre à une opération. DragSession.Phase.ended(_:)
Si votre cible de déploiement ou votre SDK ne permet pas d’utiliser le rappel SwiftUI, une source de glissement AppKit fournit le point de fin documenté. Apple précise qu’une source reçoit draggingSession(_:endedAt:operation:) lorsque son glissement se termine ou est annulé. Une source personnalisée doit elle-même lancer cette session. beginDraggingSession(with:event:source:)
Si vous avez uniquement besoin de repositionner une forme dans un même canevas, envisagez un DragGesture local au lieu d’une session système de glisser-déposer. @GestureState se réinitialise lorsque son geste devient inactif, ce qui en fait un choix adapté au retour visuel temporaire de ce geste. Distinguez ce cas d’un glissement système qui fournit du contenu transférable aux destinations. GestureState
Implémenter le nettoyage géré par la source dans SwiftUI
Créez une app SwiftUI pour macOS avec un SDK qui expose onDragSessionUpdated(_:), définissez sa cible de déploiement sur macOS 26 ou ultérieur, puis remplacez les déclarations générées de l’app et de la vue de contenu par ce programme complet. Il fournit une chaîne synthétique, shape.circle, ainsi qu’une zone de réception.
La source utilise draggable(_:) pour fournir du contenu transférable. La destination n’accepte qu’une seule chaîne correspondante et renvoie false pour toute autre entrée. Apple indique que le résultat booléen de la destination précise si le dépôt a réussi. draggable(_:), dropDestination(for:action:isTargeted:)
import SwiftUI
@main
struct DragFeedbackDemoApp: App {
var body: some Scene {
WindowGroup {
DragFeedbackDemo()
}
}
}
struct DragFeedbackDemo: View {
private let payload = "shape.circle"
@State private var isDragging = false
@State private var isTargeted = false
@State private var acceptedCopies = 0
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle into the receiving area")
Circle()
.fill(Color.blue)
.frame(width: 72, height: 72)
.opacity(isDragging ? 0.4 : 1)
.draggable(payload)
.onDragSessionUpdated { session in
switch session.phase {
case .initial, .active:
isDragging = true
case .ended, .dataTransferCompleted:
restoreFeedback()
default:
break
}
}
Text(isDragging ? "Dragging" : "Ready")
RoundedRectangle(cornerRadius: 12)
.fill(isTargeted
? Color.blue.opacity(0.2)
: Color.gray.opacity(0.12))
.overlay {
Text("Receiving area")
}
.frame(width: 280, height: 120)
.dropDestination(for: String.self) { items, _ in
guard items.count == 1,
items.first == payload else {
return false
}
acceptedCopies += 1
return true
} isTargeted: { targeted in
isTargeted = targeted
}
Text("Accepted copies: \(acceptedCopies)")
}
.padding(32)
.frame(minWidth: 380, minHeight: 400)
}
private func restoreFeedback() {
isDragging = false
isTargeted = false
}
}
Les deux indicateurs ont des rôles distincts. isDragging modifie l’opacité de la source. isTargeted modifie le remplissage de la zone de réception. L’entrée dans la zone de réception et sa sortie ne peuvent donc pas rétablir accidentellement l’apparence de la source pendant que le glissement se poursuit.
La référence des phases identifie initial comme la session sur le point de commencer, active comme son déplacement et ended comme son achèvement. L’exemple rétablit le retour visuel à ended ; il répète aussi sans risque la même réinitialisation à dataTransferCompleted. Il n’attend jamais exclusivement la fin du transfert pour rétablir l’apparence après un glissement non abouti. DragSession.Phase
Apple décrit dataTransferCompleted comme le moment où les éléments glissés ont été transférés et où les éléments temporaires peuvent être nettoyés. Cet exemple n’utilise aucune ressource d’exportation temporaire. Dans une implémentation plus large, séparez le nettoyage visuel du nettoyage des ressources afin que le rétablissement de l’opacité ne supprime rien dont un transfert a encore besoin. DragSession.Phase.dataTransferCompleted
Le cercle reste dans la hiérarchie pendant toute la session. Un dépôt accepté incrémente le nombre affiché dans la destination ; un dépôt refusé ne modifie aucun contenu. L’exemple est ainsi utile pour diagnostiquer le retour visuel indépendamment du réordonnancement ou de la sémantique de déplacement. Le code a été examiné à l’aide des déclarations récupérées ; il n’a pas été compilé ni exécuté, car l’environnement disponible est Linux et ne dispose pas de SwiftUI.
Comment le repli AppKit peut-il préserver la responsabilité des événements ?
Pour une ancienne version de macOS prise en charge, remplacez l’implémentation de la source de glissement par une NSView intégrée à l’aide de NSViewRepresentable. Le representable crée et met à jour la vue native, puis transmet ses rappels à l’état SwiftUI. SwiftUI contrôle la disposition de la vue représentée ; utilisez donc les modificateurs de dimensionnement SwiftUI plutôt que d’attribuer vous-même son cadre. NSViewRepresentable
Implémentez le repli comme une séquence gérée par une seule source :
- Dans
mouseDown(with:)de la vue source native, conservez l’événement réel de pression du bouton de la souris transmis à cette vue. - Dans
mouseDragged(with:), appliquez le seuil de glissement de votre choix et empêchez le lancement d’une seconde session. - Créez un
NSDraggingItemà partir d’un auteur de presse-papiers en mémoire contenant l’identifiant de l’élément synthétique. - Fournissez une image générée de la forme et définissez son cadre de glissement dans le système de coordonnées de la vue source.
- Appelez
beginDraggingSession(with:event:source:)en utilisant l’événement conservé de pression du bouton et l’objet qui implémenteNSDraggingSource. - Affichez le retour visuel de la source à partir de
draggingSession(_:willBeginAt:)et rétablissez-le sans condition depuisdraggingSession(_:endedAt:operation:). - Effacez l’événement d’entrée conservé et les données de suivi de session lorsque le suivi prend fin. Maintenez la source et son propriétaire de rappel en vie jusqu’à la fin de la session.
Ces détails suivent le contrat explicite d’AppKit : la méthode de démarrage de session prend l’événement de pression du bouton qui l’a déclenchée, les cadres des éléments glissés utilisent le système de coordonnées de la vue, et la source reçoit les messages de cycle de vie. NSDraggingItem accepte un objet conforme à NSPasteboardWriting ; sa méthode pratique à image unique définit le cadre de glissement et le contenu. beginDraggingSession(with:event:source:), init(pasteboardWriter:), setDraggingFrame(_:contents:)
Pour cette interaction au sein de votre app, renvoyez .copy pour NSDraggingContext.withinApplication et un ensemble d’opérations vide pour les autres contextes. Cela revient à n’offrir que des copies locales. AppKit demande à la source quelles opérations elle autorise au moyen de draggingSession(_:sourceOperationMaskFor:). draggingSession(_:sourceOperationMaskFor:)
Rétablissez l’apparence quel que soit le résultat de fin, et pas seulement après une copie réussie. Ne conditionnez les modifications du modèle qu’à l’opération réellement implémentée par votre app. Le point du rappel de fin est exprimé en coordonnées écran ; ne le comparez pas directement aux limites locales d’une cible SwiftUI. draggingSession(_:endedAt:operation:)
Une vue native transparente placée derrière un .draggable SwiftUI existant ne devient pas la source de cette session simplement parce qu’elle est conforme à NSDraggingSource. La source est l’objet transmis au démarrage de la session. Choisissez un seul propriétaire pour le lancement et la fin, et limitez la surface de glissement native à la zone d’interaction prévue de la forme.
Pourquoi éviter la surveillance du relâchement du bouton de la souris ou la désinitialisation du fournisseur ?
Un moniteur global des relâchements du bouton de la souris observe la distribution des entrées, qui constitue une limite différente de la fin du glissement. Apple précise que les moniteurs d’événements locaux ne reçoivent pas les événements consommés par des boucles de suivi imbriquées. Cette limite fait d’un moniteur un mauvais substitut au rappel documenté de fin de la source. addLocalMonitorForEvents(matching:handler:)
De même, deinit d’un fournisseur d’élément indique la durée de vie de l’objet. Swift appelle un désinitialiseur juste avant la désallocation d’une instance ; ce contrat ne permet pas de déterminer quand un glissement s’est terminé. Utiliser la destruction du fournisseur comme signal de fin lie l’état de l’interface à la possession des références plutôt qu’au cycle de vie du glissement. Deinitialization
Ajouter un second DragGesture à côté d’un glissement système pose le même problème de principe : son état appartient à ce geste. Utilisez @GestureState lorsque le geste local gère l’interaction ; utilisez le rappel de session de la source lorsque le glissement système la gère. GestureState
Vérifier le correctif avec une matrice de fin de session
Utilisez le cercle synthétique pour vérifier séparément chaque parcours. Il s’agit des comportements attendus de l’implémentation, et non de résultats de tests rapportés.
| Interaction | Retour visuel attendu de la source | Résultat attendu pour la destination |
|---|---|---|
| Déposer le cercle dans la zone de réception | Revient à la normale | Le nombre augmente de un |
| Relâcher le pointeur dans une zone vide de la fenêtre | Revient à la normale | Le nombre reste inchangé |
| Quitter la zone de réception tout en poursuivant le glissement | Reste estompée | La mise en évidence de la cible disparaît |
| Revenir dans la zone et effectuer un dépôt valide | Revient à la normale à la fin | Le nombre augmente de un |
| Relâcher le pointeur hors de la fenêtre de l’app sans destination acceptante | Revient à la normale | Le nombre reste inchangé |
| Annuler un glissement actif | Revient à la normale | Le nombre reste inchangé |
| Répéter plusieurs glissements infructueux | Chaque session rétablit l’apparence normalement | Aucun dépôt accepté ne s’accumule |
Si la source reste estompée, vérifiez d’abord que l’observateur de fin entoure la véritable source de glissement et que celle-ci reste présente. Si seule la mise en évidence de la cible persiste, examinez la gestion d’entrée et de sortie de la cible ainsi que le nettoyage partagé. Si le nombre accepté change après un dépôt refusé, examinez séparément la validation et la modification du modèle, indépendamment du rétablissement visuel.
La solution durable consiste à rendre explicite la responsabilité de la fin : la source rétablit le retour visuel de toute la session, la destination gère son propre état de survol et le contenu accepté, et le nettoyage du transfert suit son cycle de vie documenté. Une fois ces responsabilités séparées, un dépôt hors de toutes les cibles ne laisse aucun état visuel en attente d’un rappel de destination.
Sources
onDragSessionUpdated(_:) — Apple
DragSession.Phase — Apple
DragSession.Phase.ended(_:) — Apple
draggable(_:) — Apple
DropDelegate — Apple
GestureState — Apple
NSViewRepresentable — Apple
init(pasteboardWriter:) — Apple
setDraggingFrame(_:contents:) — Apple
Deinitialization — Swift.org