Pour le glisser-déposer dans votre application Mac, la source déclare les opérations prises en charge, la destination en choisit une et votre modèle applique le résultat. Avec SwiftUI sur macOS 26 ou version ultérieure, configurez les deux extrémités et ne supprimez l’original qu’une fois le déplacement terminé.
Cet article présente une petite interaction de bureau : faites glisser un cercle fictif vers « Copier ici » ou « Déplacer ici ». Tout reste en mémoire. L’exemple explicite l’opération, l’identité de l’élément et le nettoyage à la source, puis transpose ces responsabilités dans AppKit.
Sur quoi la source et la destination doivent-elles s’accorder ?
Une charge utile transférable décrit ce qui passe d’une vue à l’autre. La copie ou le déplacement décrit ce qu’il advient de l’original. Ce sont deux décisions distinctes. La documentation SwiftUI d’Apple indique que le glisser-déposer prend en charge la copie par défaut ; activer le déplacement ajoute une autre opération prise en charge. La destination choisit ensuite comment recevoir l’élément. DragConfiguration
Pour concevoir cette interaction, répartissez les responsabilités en trois volets :
| Responsabilité | Copie | Déplacement |
|---|---|---|
| Capacité de la source | Proposer la copie | Proposer le déplacement |
| Validation par la destination | Insérer un élément indépendant | Recevoir l’élément existant |
| Fin de l’opération à la source | Conserver l’original | Supprimer l’original une fois le déplacement terminé |
Dans cet exemple de cercle, une copie indépendante reçoit un nouvel identifiant. Un déplacement conserve l’identifiant tout en changeant l’emplacement. C’est un choix de conception de l’application, conforme à l’exigence de Swift selon laquelle une entité identifiable possède une identité stable ; Swift laisse à l’application le soin de définir la portée et la durée de cette identité. Identifiable
En pratique, renvoyer une opération de déplacement ne déplace pas un élément dans vos tableaux. Vous devez toujours insérer l’élément dans la destination et le supprimer de la source. Apple montre précisément comment observer la fin de l’opération de glisser pour indiquer au modèle quand supprimer un élément de la source. onDragSessionUpdated(_:)
Quelles API SwiftUI une application Mac doit-elle utiliser ?
Pour cette implémentation, ciblez macOS 26 ou une version ultérieure et utilisez un SDK qui contient les modificateurs de configuration documentés. Ajoutez draggable(_:) à la source, dragConfiguration(_:) pour déclarer ses opérations et dropConfiguration(_:) pour sélectionner l’opération de la destination. Les deux modificateurs de configuration sont documentés comme disponibles à partir de macOS 26. dragConfiguration(_:), dropConfiguration(_:)
La surcharge actuelle de dropDestination(for:isEnabled:action:) fournit une DropSession à son action. Cette action renvoie Void. Elle diffère de l’ancienne surcharge, dont l’action reçoit un emplacement et renvoie un booléen. Dans l’exemple, précisez isEnabled: pour rendre explicite la surcharge visée. dropDestination(for:isEnabled:action:)
À la destination, examinez suggestedOperations avant de sélectionner une opération. Pour une cible générale qui privilégie le déplacement, choisissez le déplacement s’il est proposé, sinon la copie si elle est proposée, sinon l’opération interdite. L’exemple documenté par Apple utilise cette solution de repli de déplacement vers copie et précise que la fermeture de configuration est appelée fréquemment : veillez donc à ce qu’elle reste peu coûteuse. dropConfiguration(_:)
Nos deux cibles ont volontairement des rôles fixes. « Copier ici » accepte la copie ; « Déplacer ici » accepte le déplacement. Une cible renvoie l’opération interdite si l’opération qu’elle représente n’est pas disponible. Le résultat est ainsi prévisible sans nécessiter de modificateur de clavier.
Créer un exemple complet de copie et de déplacement en mémoire
Créez une application SwiftUI pour macOS, définissez sa cible de déploiement sur macOS 26 ou une version ultérieure, puis remplacez les déclarations générées de l’application et de la vue de contenu par le fichier unique suivant. L’exemple transfère une chaîne intégrée qui contient l’identifiant d’un élément ; aucun type transférable ni aucune déclaration de type personnalisés ne sont donc nécessaires. Apple documente les charges utiles transférables pour les sources de glisser et les charges utiles de type chaîne pour les destinations de dépôt. draggable(_:), dropDestination(for:isEnabled:action:)
Il s’agit d’une application minimale complète, conçue pour une seule fenêtre ouverte et un seul élément source. J’ai vérifié ses références d’API à partir de la documentation récupérée ; je n’ai pas pu la compiler ni l’exécuter ici, car Swift et le SDK macOS ne sont pas disponibles.
import SwiftUI
import Foundation
@main
struct CopyMoveDemoApp: App {
var body: some Scene {
WindowGroup {
CopyMoveDemo()
}
}
}
struct CopyMoveDemo: View {
@State private var sourceID = UUID()
@State private var sourcePresent = true
@State private var copiedIDs: [UUID] = []
@State private var movedIDs: [UUID] = []
@State private var moveReceived = false
private var sourceConfiguration: DragConfiguration {
DragConfiguration(
operationsWithinApp: .init(allowMove: true),
operationsOutsideApp: .init(
allowCopy: false,
allowMove: false,
allowDelete: false
)
)
}
var body: some View {
VStack(spacing: 24) {
Text("Drag the circle to a target")
.font(.headline)
if sourcePresent {
Circle()
.fill(.blue)
.frame(width: 90, height: 90)
.overlay(Text("Circle").foregroundStyle(.white))
.draggable(sourceID.uuidString)
.dragConfiguration(sourceConfiguration)
.onDragSessionUpdated { session in
if session.phase == .initial {
moveReceived = false
}
if session.phase == .ended(.move), moveReceived {
sourcePresent = false
}
if case .ended = session.phase {
moveReceived = false
}
}
} else {
Text("Original moved")
.frame(width: 90, height: 90)
}
HStack(alignment: .top, spacing: 20) {
target(moving: false)
target(moving: true)
}
}
.padding(24)
.frame(minWidth: 520, minHeight: 320)
}
private func target(moving: Bool) -> some View {
let operation: DropOperation = moving ? .move : .copy
let ids = moving ? movedIDs : copiedIDs
return VStack(spacing: 10) {
Text(moving ? "Move here" : "Copy here")
.font(.headline)
ForEach(ids, id: \.self) { id in
Text("Circle \(id.uuidString.prefix(4))")
}
Spacer(minLength: 20)
}
.frame(width: 210, height: 150)
.background(.blue.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(for: String.self, isEnabled: true) {
tokens, session in
guard session.localSession != nil,
sourcePresent,
tokens == [sourceID.uuidString] else {
return
}
if moving {
guard !movedIDs.contains(sourceID) else { return }
movedIDs.append(sourceID)
moveReceived = true
} else {
copiedIDs.append(UUID())
}
}
.dropConfiguration { session in
guard sourcePresent,
session.localSession != nil,
session.itemsCount == 1,
session.suggestedOperations.contains(operation) else {
return DropConfiguration(operation: .forbidden)
}
return DropConfiguration(operation: operation)
}
}
}
La configuration de la source propose la copie et le déplacement dans l’application. Elle désactive les opérations en dehors de l’application à l’aide de l’initialiseur macOS prévu à cet effet. La destination vérifie également localSession, qu’Apple indique comme absent pour un glisser provenant d’une autre application. Ces paramètres permettent de se concentrer sur l’élément en mémoire de la fenêtre de l’exemple. DragConfiguration.OperationsWithinApp, init(allowCopy:allowMove:allowDelete:), DropSession
L’action de dépôt valide le jeton exact avant toute insertion. La copie ajoute un nouvel identifiant. Le déplacement ajoute l’identifiant d’origine et enregistre que cette destination l’a reçu. La source ne supprime ensuite l’original affiché que si la session se termine par un déplacement et que cette réception a été enregistrée. Pour cette démonstration limitée, cette réception constitue une protection gérée par l’application, et non une fonctionnalité de transaction SwiftUI.
La déclaration actuelle de la phase de glisser d’Apple comprend initial, active, ending, ended et dataTransferCompleted. L’exemple utilise le cas documenté ended(DropOperation) pour obtenir le résultat de l’opération. Il ne supprime pas la source pendant le survol ni lors de la phase de fin précédente. DragSession.Phase
Définir clairement les responsabilités et les limites des gestes
La chaîne sert de jeton de recherche, et non de modèle complet du cercle. La destination la retrouve dans l’état de la source détenu par cette vue. Pour plusieurs documents ou fenêtres, remplacez cette recherche locale par un propriétaire de modèle explicite et une réception associée à la session de glisser et aux éléments concernés. Le fait qu’un glisser provienne de l’application n’indique pas à lui seul quel document possède un élément.
Pour plusieurs éléments déplaçables, utilisez des identifiants de modèle stables et, lorsque c’est possible, récupérez les identifiants déplacés depuis la session. Apple fournit draggedItemIDs(for:) pour les charges utiles identifiables ou les identifiants fournis séparément. Évitez de supprimer l’élément qui se trouve sélectionné au moment où l’opération se termine : la sélection et l’identité de l’élément déplacé sont deux données distinctes. DragSession
Laissez le modificateur SwiftUI draggable(_:) installer l’interaction de glisser. La création de la charge utile ne doit entraîner aucune suppression du modèle. Apple indique que ce modificateur ajoute les gestes appropriés et déconseille d’envoyer une fermeture d’export vers l’acteur principal. draggable(_:)
Comment appliquer le même accord dans AppKit ?
Utilisez AppKit si votre cible de déploiement ou l’architecture de vues existante nécessite un cycle de vie de glisser explicite. Son protocole de source déclare les opérations autorisées via draggingSession(_:sourceOperationMaskFor:). Le contexte permet de distinguer les destinations situées dans l’application de celles qui se trouvent en dehors. Pour l’interaction ci-dessus, proposez la copie et le déplacement dans l’application, et aucune opération en dehors. draggingSession(_:sourceOperationMaskFor:)
La destination s’enregistre pour le type de presse-papiers concerné, puis choisit une opération dans draggingEntered(_:) et met ce choix à jour si nécessaire dans draggingUpdated(_:). Apple demande à la destination de tenir compte du chevauchement entre les opérations de la source et ses propres opérations et types de données pris en charge. Renvoyez une seule opération, ou aucune s’il n’existe aucun chevauchement acceptable. NSDraggingDestination, draggingEntered(_:)
Effectuez l’insertion dans performDragOperation(_:), une fois la préparation réussie. Ne renvoyez true que si la destination accepte les données. La source reçoit l’opération terminée via draggingSession(_:endedAt:operation:) ; utilisez ce résultat, ainsi que les règles de propriété de votre modèle, pour achever la suppression côté source lors d’un déplacement. performDragOperation(_:), draggingSession(_:endedAt:operation:)
Si vous intégrez une surface de glisser AppKit dans une interface SwiftUI, laissez cette surface gérer l’interaction AppKit. beginDraggingSession(with:event:source:) prend l’événement initial de pression de souris, et les cadres des éléments déplacés utilisent le système de coordonnées de la vue. Préservez cette relation entre événement et coordonnées au lieu de fabriquer un événement à partir d’une valeur ultérieure d’un geste SwiftUI. beginDraggingSession(with:event:source:)
L’ancien code de destination SwiftUI peut renvoyer DropProposal(operation: .move) depuis DropDelegate.dropUpdated(info:). Cela exprime la proposition de la destination. Cela ne remplace ni la configuration des capacités de la source, ni l’observation de l’opération terminée, ni les modifications de votre modèle. Si les nouvelles API SwiftUI de source ne sont pas disponibles et que vous avez besoin d’un contrôle explicite des deux extrémités, le cycle de vie de source et de destination d’AppKit offre cette possibilité. dropUpdated(info:), NSDraggingSource
Vérifier ensemble l’opération et le modèle
Pour l’exemple, utilisez la liste des états attendus ci-dessous. Il s’agit de critères d’acceptation déduits de l’implémentation, et non de résultats de tests rapportés :
| Action | Original | Cible de copie | Cible de déplacement |
|---|---|---|---|
| Déposer une fois sur « Copier ici » | Reste | Reçoit un nouvel identifiant | Inchangée |
| Copier à nouveau | Reste | Reçoit un autre nouvel identifiant | Inchangée |
| Déposer sur « Déplacer ici » | Disparaît une fois le déplacement terminé | Inchangée | Reçoit l’identifiant d’origine |
| Annuler ou relâcher ailleurs que sur les deux cibles | Reste | Inchangée | Inchangée |
Si le comportement ne correspond pas, examinez les étapes dans l’ordre : capacités de la source, opération de la destination, insertion dans la destination, puis fin de l’opération à la source. Si l’original est dupliqué après un déplacement, le nettoyage à la source est probablement manquant. Deux suppressions indiquent que plusieurs propriétaires du modèle interviennent. Une copie inattendue peut signifier que la source n’a pas proposé le déplacement ou que la destination a choisi la copie.
La responsabilité finale est claire : l’opération négociée décrit le transfert, tandis que votre modèle gère l’emplacement et l’identité de l’élément. Proposez les opérations prises en charge, choisissez une opération autorisée à la destination et effectuez chaque modification du modèle une seule fois, au moment prévu par le cycle de vie.
Sources
DragConfiguration — Apple
Identifiable — Apple
onDragSessionUpdated(_:) — Apple
dragConfiguration(_:) — Apple
dropConfiguration(_:) — Apple
draggable(_:) — Apple
DropSession — Apple
DragSession.Phase — Apple
DragSession — Apple
NSDraggingDestination — Apple
draggingEntered(_:) — Apple
performDragOperation(_:) — Apple
dropUpdated(info:) — Apple
NSDraggingSource — Apple