Garder la position validée de la forme inchangée pendant que SwiftUI prévisualise le glisser, puis valider la translation finale dans onEnded. Cela conserve la position de départ jusqu'à la fin sans dépendre du timing du relâchement de la souris d'AppKit. Voici un exemple complet pour déplacer un rectangle dans votre propre application Mac.
Quelle limite de completion doit posséder l'état de départ ?
Pour une interaction de déplacement de forme, la tâche concrète consiste à calculer chaque prévisualisation et la position finale à partir de la même position de départ. La limite de completion utile est la réussite de la completion du geste de cette forme. Apple documente que onEnded(_:) fournit la valeur finale du geste et ne s'exécute que lorsque le geste se termine avec succès. Il recommande @GestureState pour l'état temporaire qui doit être réinitialisé quelle que soit la façon dont le geste se termine. Documentation onEnded(_:) d'Apple.
Donnez à l'interaction deux types d'état : une position de modèle validée et une translation de prévisualisation temporaire. Pendant le glissement, affichez leur somme. À la completion réussie, appliquez la translation finale à la position validée une seule fois. C'est un design dérivé de la distinction d'Apple entre les mises à jour de geste transitoires et les mises à jour d'état permanentes. Ajout d'interactivité avec les gestes.
| Information | Propriétaire dans cet exemple | Durée de vie |
|---|---|---|
| Centre validé du rectangle | Modèle @State privé |
Survit à la completion du geste |
| Translation de prévisualisation actuelle | @GestureState |
Réinitialisé quand le geste devient inactif |
| Translation finale | argument de callback onEnded |
Utilisé de manière synchrone pour valider le déplacement |
| Événement mouse-up AppKit | Gestion d'événements AppKit | Signale un relâchement de bouton ; ne définit pas la completion du geste SwiftUI |
Apple documente @State comme un stockage géré par SwiftUI et une source de vérité pour les valeurs dans une hiérarchie de vues. @GestureState se réinitialise automatiquement lorsque son geste devient inactif. Conserver la ligne de base dans le premier et la prévisualisation dans le second donne à chaque valeur la durée de vie dont cette interaction a besoin. State, GestureState.
Construire un exemple complet de glisser-rectangle sur Mac
Créez une application SwiftUI macOS dans Xcode avec une cible de déploiement macOS 14 ou ultérieure. Remplacez la déclaration d'application générée et la vue de contenu par le fichier unique suivant, en vous assurant que la cible n'a qu'une seule déclaration @main. L'exemple utilise un rectangle synthétique stocké entièrement en mémoire. Ses APIs d'espaces de coordonnées nommés sont documentées comme disponibles à partir de macOS 14. Initialiseur de geste de glisser, Espaces de coordonnées nommés.
import SwiftUI
@main
struct GestureStartDemoApp: App {
var body: some Scene {
WindowGroup {
ShapeCanvas()
}
}
}
private struct SampleItem {
var center = CGPoint(x: 160, y: 140)
}
private struct ShapeCanvas: View {
@State private var item = SampleItem()
@GestureState private var previewTranslation = CGSize.zero
private let canvasWidth: CGFloat = 600
private let canvasHeight: CGFloat = 360
private let shapeWidth: CGFloat = 80
private let shapeHeight: CGFloat = 60
private func projectedCenter(_ translation: CGSize) -> CGPoint {
let proposedX = item.center.x + translation.width
let proposedY = item.center.y + translation.height
let halfWidth = shapeWidth / 2
let halfHeight = shapeHeight / 2
return CGPoint(
x: min(max(proposedX, halfWidth),
canvasWidth - halfWidth),
y: min(max(proposedY, halfHeight),
canvasHeight - halfHeight)
)
}
private var moveGesture: some Gesture {
DragGesture(
minimumDistance: 3,
coordinateSpace: .named("shapeCanvas")
)
.updating($previewTranslation) { value, translation, _ in
translation = value.translation
}
.onEnded { value in
let finalCenter = projectedCenter(value.translation)
item.center = finalCenter
}
}
var body: some View {
let displayedCenter = projectedCenter(previewTranslation)
VStack(alignment: .leading, spacing: 12) {
Text("Drag the blue rectangle")
.font(.headline)
ZStack {
Color.gray.opacity(0.12)
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: shapeWidth, height: shapeHeight)
.contentShape(Rectangle())
.gesture(moveGesture)
.position(
x: displayedCenter.x,
y: displayedCenter.y
)
}
.frame(width: canvasWidth, height: canvasHeight)
.coordinateSpace(.named("shapeCanvas"))
.clipped()
Text(
"Committed center: " +
"\(Int(item.center.x)), \(Int(item.center.y))"
)
.monospacedDigit()
}
.padding(20)
}
}
L'invariant clé est que item.center ne change jamais dans updating. Il reste la ligne de base du modèle du geste jusqu'à ce que onEnded calcule finalCenter. Le calcul final lit value.translation directement, il n'a donc pas besoin que la propriété de prévisualisation conserve sa dernière valeur pendant la completion.
Le geste mesure le mouvement dans l'espace de coordonnées nommé du canevas englobant. Le position(x:y:) du rectangle place son centre dans ce même espace parent. Faire correspondre ces espaces évite de mélanger les coordonnées locales d'une forme en mouvement avec les coordonnées du canevas. Apple documente à la fois la mesure de geste dans un espace nommé et le positionnement basé sur le centre. coordinateSpace(_:), position(x:y:).
La fonction partagée projectedCenter garde également le rectangle à l'intérieur du canevas. La prévisualisation et la validation utilisent des bornes identiques : centres horizontaux de 40 à 560, et centres verticaux de 30 à 330. Au bord, le mouvement reste basé sur le déplacement total depuis le début du glisser original ; le rectangle s'éloigne du bord une fois que cette position proposée revient à l'intérieur des bornes.
Un mouvement plus petit que le seuil de reconnaissance de 3 points configuré ne constitue pas un glisser reconnu. Cet exemple traite délibérément le début comme le commencement d'une interaction de déplacement de forme reconnue. Une exigence de capturer l'état du modèle à l'événement physique mouse-down est une limite différente. Initialiseur de geste de glisser.
Pourquoi la position de départ survit à la mise à jour finale
Apple définit DragGesture.Value.translation comme la translation totale depuis le début du geste jusqu'à son événement actuel. Elle est cumulative, plutôt qu'un delta depuis le callback précédent. Par conséquent, ajouter chaque translation rapportée à une position de modèle déjà mise à jour surcompterait le mouvement. DragGesture.Value.
Considérez ces entrées explicitement illustratives, toutes dans les limites du canevas :
| Étape | Translation totale rapportée | Centre validé | Centre affiché |
|---|---|---|---|
| Avant le glisser | (0, 0) |
(160, 140) |
(160, 140) |
| Première mise à jour | (12, 5) |
(160, 140) |
(172, 145) |
| Deuxième mise à jour | (30, -10) |
(160, 140) |
(190, 130) |
| Fin réussie | (45, 20) |
Devient (205, 160) |
Se stabilise à (205, 160) |
Chaque prévisualisation utilise la même ligne de base. La completion applique la (45, 20) finale une fois. Un glisser réussi ultérieur avec translation (-15, 10) commence alors depuis (205, 160) et valide (190, 170). Ces calculs ont été vérifiés avec un calcul Python déterministe. L'application SwiftUI a été inspectée par rapport à la documentation d'API récupérée mais n'a pas été compilée ni exécutée dans cet environnement.
Ne remplacez pas startLocation par le centre de départ du rectangle. Apple définit startLocation comme la position du premier événement du glisser. Un pointeur peut commencer près du coin d'un rectangle, donc cette position et le centre du modèle de la forme peuvent différer. De même, predictedEndTranslation est une prédiction basée sur la vitesse ; utilisez la translation finale pour cette interaction de placement direct. DragGesture.Value.
L'annulation ne nécessite pas de restauration de la position validée dans ce design car les mises à jour de prévisualisation ne l'écrasent jamais. Apple documente que l'état du geste se réinitialise lorsque le geste devient inactif, alors que onEnded ne s'exécute qu'après le succès. Le déplacement temporaire disparaît donc sans valider un mouvement annulé. Cette conclusion découle de ces comportements documentés ; elle ne dépend pas de l'assomption d'un ordre entre la réinitialisation et le callback de completion réussie. GestureState, onEnded(_:).
Pourquoi un moniteur mouse-up AppKit ne devrait pas effacer la ligne de base
Un moniteur d'événements local AppKit reçoit les événements avant leur distribution à l'application. Effacer l'état de début de geste partagé à partir d'un événement mouse-up surveillé peut donc le supprimer avant que le traitement en aval ne traite cet événement. C'est une conséquence de la limite de distribution documentée, pas une garantie documentée sur l'ordre des callbacks SwiftUI. addLocalMonitorForEvents(matching:handler:).
De même, NSResponder.mouseUp(with:) signale que l'utilisateur a relâché le bouton gauche de la souris. Son contrat ne déclare pas que chaque callback de geste SwiftUI ou mise à jour de vue s'est terminé. Gardez l'observation d'événements bruts et la completion de geste reconnu comme des responsabilités distinctes. mouseUp(with:).
Les APIs examinées ici n'établissent pas de signification générale de callback indiquant que SwiftUI a terminé tout le traitement associé à un événement brut. Un bloc de nettoyage mis en file d'attente ou un minuteur court ajouterait une hypothèse d'ordonnancement sans établir cette relation. Pour cette tâche, la voie prise en charge consiste à effectuer le calcul final du modèle à l'intérieur de onEnded, où la valeur finale du geste est disponible.
Si l'interaction doit être implémentée dans une vue AppKit personnalisée, laissez cette vue posséder sa séquence d'entrée et son état de session, et l'intégrez via NSViewRepresentable. Apple fournit des points d'accroche de création, mise à jour, coordinateur et démantèlement pour cette intégration. Gardez la session de la vue représentée dans la vue AppKit ou son coordinateur plutôt que de la reconstruire à partir de chaque valeur representable. SwiftUI contrôle la mise en page de la vue représentée, donc ne modifiez pas indépendamment son cadre ou ses limites gérés. NSViewRepresentable.
Quand l'interaction a-t-elle besoin d'un instantané explicite ?
La position validée de l'exemple est suffisante car aucune autre action ne la modifie pendant que le glisser est actif. Pour un canevas plus grand, utilisez cet aide à la décision avant d'introduire un objet de session distinct :
| Exigence | Conséquence de conception |
|---|---|
| Déplacer un élément dont la position validée reste inchangée pendant la prévisualisation | Utiliser l'approche ligne de base + prévisualisation démontrée |
| Déplacer plusieurs éléments sélectionnés ensemble | Conserver leurs identifiants stables et positions de départ comme un instantané d'interaction unique |
| Permettre à d'autres commandes de modifier les positions pendant le glisser | Définir s'il faut bloquer ces modifications, annuler le glisser, ou résoudre par rapport à une révision du modèle |
| Capturer l'état au mouse-down physique avant la reconnaissance | Utiliser une limite d'entrée AppKit explicitement possédée ; la première mise à jour de geste n'est pas équivalente |
| Garder une session distincte après la completion | Lui donner un propriétaire explicite et une politique de terminaison plutôt que de compter sur l'état de geste temporaire |
Un instantané doit préserver les valeurs. Les structures Swift ont une sémantique de valeur, alors qu'assigner une instance de classe copie sa référence. Par conséquent, un tableau de références à des objets d'éléments mutables ne gèle pas leurs positions par lui-même. Pour un déplacement multi-éléments, capturez les identifiants et les valeurs de coordonnées nécessaires au calcul final. Structures et Classes — Le langage de programmation Swift.
Si vous introduisez une session persistante, définissez explicitement la completion réussie, l'annulation et la suppression de vue. La nettoyer seulement dans onEnded laisse l'annulation non traitée, car ce callback est uniquement pour le succès. Ne réutilisez pas la transaction de réinitialisation d'état de geste comme notification de completion universelle : Apple documente sa fermeture de réinitialisation comme fournissant une transaction pour réinitialiser l'état de geste. onEnded(_:), init(wrappedValue:reset:).
Pour la tâche ordinaire de déplacement de rectangle, préservez la ligne de base en laissant le modèle validé intact pendant la prévisualisation. Calculez la position finale à partir de la propre valeur du geste réussi, validez-la une fois, et laissez l'état de geste temporaire se réinitialiser. Cela donne à la position de départ un propriétaire clair et une durée de vie à travers le calcul qui en a besoin.
Sources
- Ajout d'interactivité avec les gestes — Apple.
- State — Apple.
- GestureState — Apple.
- onEnded(_:) — Apple.
- DragGesture.Value — Apple.
- init(minimumDistance:coordinateSpace:) — Apple.
- coordinateSpace(_:) — Apple.
- position(x:y:) — Apple.
- addLocalMonitorForEvents(matching:handler:) — Apple.
- mouseUp(with:) — Apple.
- NSViewRepresentable — Apple.
- Structures et Classes — Le langage de programmation Swift — Projet Swift.org.
- init(wrappedValue:reset:) — Apple.