Pour réorganiser une liste dans votre application Mac, transmettez directement la source et la destination de onMove de SwiftUI à move(fromOffsets:toOffset:). La destination indique une limite dans la collection d’origine ; soustraire manuellement un pour les déplacements vers le bas produit donc un résultat incorrect.
Ce guide crée une petite liste macOS native avec des libellés synthétiques, explique le calcul de la destination et fournit une matrice de tests pour diagnostiquer les lignes mal placées. Il porte sur la réorganisation des éléments dans la liste de votre propre application.
Que signifie la destination de onMove ?
Apple définit les deux arguments de onMove(perform:) comme des décalages relatifs à la collection sous-jacente de la vue dynamique. Le premier argument est un IndexSet de décalages des sources ; le second est le décalage de destination. Ces positions appartiennent à la collection utilisée par ce ForEach particulier. Documentation d’Apple sur onMove(perform:).
Pour move(fromOffsets:toOffset:), Apple définit la destination comme le décalage avant lequel les éléments déplacés doivent être insérés ; count peut également désigner la fin. Voyez cela comme une limite entre les éléments de la collection d’origine. Documentation d’Apple sur move(fromOffsets:toOffset:).
Considérons l’ordre d’origine A, B, C, D, E. Les décalages des éléments sont 0, 1, 2, 3, 4, tandis que les limites d’insertion vont de 0 à 5. La limite 4 se trouve juste avant E ; la limite 5 se trouve après le dernier élément.
Déplacer B depuis le décalage source 1 vers la destination 4 produit donc A, C, D, B, E. L’élément déplacé se retrouve à l’indice 3, alors que l’argument de destination était 4. Les deux valeurs utilisent des positions à partir de zéro ; elles décrivent des étapes différentes de l’opération.
Cette distinction explique l’erreur fréquente lors d’un déplacement vers le bas. Le code qui transforme la destination 4 en 3 avant d’appeler la méthode de déplacement de la collection demande une autre limite et place B avant D. Lorsque vous utilisez la méthode de déplacement d’Apple, transmettez les valeurs du rappel sans les modifier.
Créer une liste macOS réorganisable complète
Utilisez dans Xcode un projet d’application SwiftUI native pour macOS. L’exemple complet ci-dessous cible macOS 11 ou une version ultérieure, car il utilise le cycle de vie App de SwiftUI ; Apple indique que onMove est disponible à partir de macOS 10.15. Remplacez les déclarations d’application et de vue de contenu générées par ce code, en ne conservant qu’un seul point d’entrée @main dans la cible. Documentation d’Apple sur App, documentation d’Apple sur onMove(perform:).
import SwiftUI
import Foundation
struct DemoItem: Identifiable {
let id: Int
let label: String
static let samples: [DemoItem] = [
DemoItem(id: 0, label: "A"),
DemoItem(id: 1, label: "B"),
DemoItem(id: 2, label: "C"),
DemoItem(id: 3, label: "D"),
DemoItem(id: 4, label: "E")
]
}
@main
struct ReorderDemoApp: App {
var body: some Scene {
WindowGroup {
ReorderView()
}
}
}
struct ReorderView: View {
@State private var items = DemoItem.samples
@State private var selection = Set<Int>()
@State private var lastMove = "No move yet"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Drag rows to change their order")
List(selection: $selection) {
ForEach(items) { item in
Text(item.label)
.tag(item.id)
}
.onMove(perform: moveItems)
}
Text("Order: " + items.map(\.label).joined(separator: ", "))
Text(lastMove)
Button("Reset") {
items = DemoItem.samples
selection.removeAll()
lastMove = "No move yet"
}
}
.padding()
.frame(minWidth: 420, minHeight: 300)
}
private func moveItems(from source: IndexSet, to destination: Int) {
lastMove = "Source: \(Array(source)); destination: \(destination)"
items.move(fromOffsets: source, toOffset: destination)
}
}
Le ForEach affiche le même tableau que celui modifié par le gestionnaire. Chaque élément possède un identifiant fixe, indépendant de sa position actuelle. Apple exige que les éléments soient identifiables ou qu’une clé d’identité explicite soit fournie à ForEach ; conserver chaque identifiant pendant un déplacement permet à la vue de continuer à représenter le même élément. Documentation d’Apple sur ForEach.
Le tableau est un état local à la vue. Sa modification entraîne la mise à jour par SwiftUI des vues qui en dépendent, notamment la liste et l’ordre affiché. Cet exemple conserve l’ordre en mémoire et n’inclut aucune implémentation de persistance. Documentation d’Apple sur State.
Lancez l’application, faites glisser B jusqu’à la limite juste avant E, puis vérifiez l’ordre et l’affichage du rappel. Utilisez Reset avant d’essayer un autre scénario. La liaison de sélection explicite permet également de sélectionner plusieurs lignes ; Apple documente les ensembles de sélection et la sélection sur Mac sans passer en mode édition. Documentation d’Apple sur List.
N’ajoutez pas EditButton() à cet exemple macOS natif. Les informations de disponibilité actuelles d’Apple incluent iOS et Mac Catalyst, mais excluent macOS natif. Avant de copier dans une cible Mac les commandes d’édition d’un tutoriel mobile, vérifiez qu’elles y sont disponibles. Documentation d’Apple sur EditButton.
Comment calculer l’indice d’insertion final ?
Ce calcul n’est nécessaire que si vous implémentez vous-même la suppression et l’insertion, ou si vous traduisez le déplacement en une autre opération sur le modèle. Il s’agit d’un modèle explicatif dérivé de la sémantique d’insertion documentée par Apple, et non d’un ajustement supplémentaire à appliquer avant d’appeler la méthode de déplacement intégrée.
Le calcul est le suivant :
indice d’insertion après suppression = destination d’origine
- nombre de décalages source inférieurs à la destination
Pour la source {1} et la destination 4, un élément supprimé se trouvait initialement avant la limite. Le tableau réduit est A, C, D, E, et l’indice d’insertion est 4 - 1 = 3. Y insérer B produit A, C, D, B, E.
Avec plusieurs sources, une simple correction vers le bas ne suffit pas. Pour les sources {1, 3} et la destination 5, les éléments déplacés sont B, D ; le tableau restant est A, C, E. Deux décalages source précèdent la destination 5, l’indice d’insertion est donc 3. Le résultat est A, C, E, B, D.
Une implémentation personnalisée doit d’abord capturer les éléments déplacés dans leur ordre d’origine, puis supprimer les originaux sans que les suppressions précédentes invalident les positions source suivantes. Elle peut ensuite calculer la position d’insertion ajustée et insérer le bloc capturé en une seule fois. Pour cette liste fondée sur un tableau, la méthode existante d’Apple évite d’implémenter vous-même ces étapes.
Quels cas limites faut-il tester ?
Les cas suivants illustrent les calculs pour l’ordre d’origine A, B, C, D, E. Rétablissez cet ordre avant chaque cas. La dernière colonne indique le résultat attendu selon le modèle, indépendamment de l’apparence de l’indicateur de dépôt.
| Décalages source | Destination d’origine | Indice d’insertion après suppression | Ordre attendu |
|---|---|---|---|
{1} |
4 |
3 |
A, C, D, B, E |
{3} |
1 |
1 |
A, D, B, C, E |
{1} |
5 |
4 |
A, C, D, E, B |
{1} |
1 |
1 |
A, B, C, D, E |
{1} |
2 |
1 |
A, B, C, D, E |
{1, 3} |
5 |
3 |
A, C, E, B, D |
{1, 3} |
3 |
2 |
A, C, B, D, E |
Les deux cas sans effet portant sur un seul élément sont importants : insérer un élément à sa propre limite ou à la limite suivante ne change pas son ordre. Apple documente explicitement ce comportement. La limite de fin est également valide, même si elle ne peut pas servir d’indice pour accéder à un élément. Documentation d’Apple sur move(fromOffsets:toOffset:).
Le modèle arithmétique de ces sept cas a été vérifié à l’aide d’un petit calcul Python déterministe. Le programme SwiftUI a été examiné, mais n’a pas été compilé ni exécuté ici, car aucun environnement d’exécution macOS ni aucune chaîne d’outils Swift n’étaient disponibles. Vérifiez le comportement réel du glisser-déposer sur la version de macOS ciblée.
Pourquoi les lignes se déplacent-elles encore incorrectement ?
Commencez par vérifier la limite de la collection, puis examinez l’identité et la mutation. Cette séquence permet de distinguer les problèmes de calcul de destination des incohérences entre les lignes affichées et les données stockées.
- Vérifiez la collection transmise à
ForEach. Si elle affiche un tableau filtré, ses décalages décrivent ce tableau filtré. Par exemple, les lignes visiblesB, Dont les décalages0, 1; appliquer la source0au tableau complet déplaceraitA. Cette conséquence découle du contrat de rappel relatif à la collection documenté par Apple. Documentation d’Apple suronMove(perform:). - Vérifiez si le rendu trie à nouveau les données. Si chaque redessin trie les éléments par libellé, modifier l’ordre du tableau sous-jacent ne peut pas établir un ordre d’affichage manuel durable. Définissez une règle de tri manuel avant d’activer la réorganisation dans cette vue.
- Vérifiez les identifiants. Conservez l’identifiant de chaque élément pendant son déplacement. Ne régénérez pas les identifiants lors du rendu et n’utilisez pas des positions changeantes comme identité des éléments déplaçables. Documentation d’Apple sur
ForEach. - Vérifiez qu’il n’y a pas de double opération. Supprimez toute soustraction manuelle de la destination, puis vérifiez que le gestionnaire applique le déplacement une seule fois à la collection de référence. Un second déplacement agit sur un ordre déjà modifié.
Pour les interfaces filtrées ou organisées en sections, définissez le comportement attendu avant de traduire les indices. Décidez si un déplacement réorganise uniquement les éléments visibles, modifie toute la collection ou transfère un élément d’une section à une autre. Faites correspondre les éléments visibles aux identités du modèle et déterminez la destination en fonction de cette règle. Un décalage numérique seul ne peut pas exprimer tous ces choix.
Conservez les décalages du rappel liés à l’instantané de la collection qu’ils décrivent. Si l’application diffère l’opération alors que d’autres modifications changent le tableau, identifiez à nouveau les éléments visés et la destination au lieu de réutiliser des positions périmées. L’exemple évite cette complication en modifiant immédiatement son tableau local.
AppKit ou TextKit modifient-ils les règles de destination ?
Pour une application qui utilise déjà NSTableView, AppKit définit son propre contrat de dépôt. NSTableView.DropOperation.above signifie une insertion au-dessus d’une ligne, tandis que .on signifie un dépôt sur cette ligne. Apple décrit un dépôt sous la dernière ligne comme une ligne n avec l’opération .above, où n est le nombre de lignes. Interprétez la ligne et l’opération avant de traduire un dépôt AppKit en déplacement dans le modèle. Documentation d’Apple sur NSTableView.DropOperation.
TextKit traite du stockage, de la mise en page et de la modification du texte au moyen de composants comme NSTextView et les gestionnaires de mise en page du texte. Il ne fournit pas le contrat de réorganisation de tableau utilisé dans cet exemple. Si une ligne contient également un éditeur, séparez son implémentation de l’édition du texte de l’opération sur la collection qui modifie l’ordre des lignes. Documentation d’Apple sur TextKit.
Pour une liste Mac classique, conservez une collection unique à ordre manuel, préservez les identifiants des éléments et transmettez directement les arguments de onMove à la méthode de déplacement de la collection. Utilisez le calcul des limites et la matrice de tests lorsque vous devez diagnostiquer le résultat ou traduire l’opération en une autre forme de modèle.
Sources
- onMove(perform:) — Apple
- move(fromOffsets:toOffset:) — Apple
- App — Apple
- ForEach — Apple
- State — Apple
- List — Apple
- EditButton — Apple
- NSTableView.DropOperation — Apple
- TextKit — Apple