Om een lijst in je Mac-app te herschikken, geef je de bron- en bestemmingswaarden van SwiftUI’s onMove rechtstreeks door aan move(fromOffsets:toOffset:). De bestemming markeert een grens in de oorspronkelijke collectie, dus handmatig één aftrekken bij verplaatsingen naar beneden levert het verkeerde resultaat op.
In deze walkthrough bouwen we een kleine native macOS-lijst met synthetische labels, leggen we de bestemmingsberekening uit en geven we een testmatrix om verkeerd geplaatste rijen op te sporen. De reikwijdte beperkt zich tot het herschikken van items in de lijst van je eigen app.
Wat betekent de onMove-bestemming?
Apple definieert de twee argumenten van onMove(perform:) als offsets ten opzichte van de onderliggende collectie van de dynamische weergave. Het eerste argument is een IndexSet met bronoffsets; het tweede is de bestemmingsoffset. Deze posities horen bij de collectie die door die specifieke ForEach wordt gebruikt. Apple’s documentatie over onMove(perform:).
Voor move(fromOffsets:toOffset:) definieert Apple de bestemming als de offset vóór welke de verplaatste elementen moeten worden ingevoegd; count mag ook het einde aanduiden. Zie het als een grens tussen elementen in de oorspronkelijke collectie. Apple’s documentatie over move(fromOffsets:toOffset:).
Neem de oorspronkelijke volgorde A, B, C, D, E. De elementoffsets zijn 0, 1, 2, 3, 4, terwijl de invoeggrenzen lopen van 0 tot en met 5. Grens 4 ligt direct vóór E; grens 5 ligt na het laatste item.
Als je B van bronoffset 1 naar bestemming 4 verplaatst, krijg je dus A, C, D, B, E. Het verplaatste item komt op index 3 terecht, hoewel het bestemmingsargument 4 was. Beide waarden gebruiken posities die bij nul beginnen; ze beschrijven verschillende fasen van de bewerking.
Dat verschil verklaart de veelvoorkomende fout bij verplaatsingen naar beneden. Code die bestemming 4 verandert in 3 voordat de verplaatsmethode van de collectie wordt aangeroepen, vraagt om een andere grens en plaatst B vóór D. Geef de callbackwaarden ongewijzigd door wanneer je Apple’s verplaatsmethode gebruikt.
Bouw een complete, herschikbare macOS-lijst
Gebruik in Xcode een native macOS SwiftUI-app-project. Het volledige voorbeeld hieronder vereist macOS 11 of nieuwer, omdat het de SwiftUI-levenscyclus van App gebruikt; Apple vermeldt dat onMove zelf beschikbaar is vanaf macOS 10.15. Vervang de gegenereerde app- en content-view-declaraties door deze code en zorg dat er in het target precies één @main-entrypoint staat. Apple’s documentatie over App, Apple’s documentatie over 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)
}
}
De ForEach toont dezelfde array die de handler wijzigt. Elk item heeft een vaste identificatie die onafhankelijk is van de huidige positie. Apple vereist identificeerbare elementen of een expliciete identiteitssleutel voor ForEach; door de identificatie tijdens een verplaatsing te behouden, kan de weergave hetzelfde item blijven weergeven. Apple’s documentatie over ForEach.
De array is lokale view-state. Als deze verandert, werkt SwiftUI de afhankelijke weergaven bij, waaronder de lijst en de getoonde volgorde. Dit voorbeeld bewaart de volgorde in het geheugen en bevat geen implementatie voor persistentie. Apple’s documentatie over State.
Start de app, sleep B naar de grens direct vóór E en controleer zowel de volgorde als de callbackweergave. Gebruik Reset voordat je een ander scenario probeert. De expliciete selectiebinding ondersteunt ook het selecteren van meerdere rijen; Apple documenteert selectiesets en selectie op de Mac zonder de bewerkingsmodus te openen. Apple’s documentatie over List.
Voeg in dit native macOS-voorbeeld geen EditButton() toe. Apple’s actuele beschikbaarheidsinformatie vermeldt iOS en Mac Catalyst, maar geen native macOS. Neem de mobiele bewerkingsknoppen uit een tutorial niet over in een Mac-target zonder eerst de beschikbaarheid te controleren. Apple’s documentatie over EditButton.
Hoe bereken je de uiteindelijke invoegindex?
Je hebt deze berekening alleen nodig als je het verwijderen en invoegen zelf implementeert, of als je de verplaatsing vertaalt naar een andere modelbewerking. Het is een verklarend model op basis van Apple’s gedocumenteerde invoegsemantiek, geen extra correctie die je moet toepassen vóór je de ingebouwde verplaatsmethode aanroept.
De berekening is:
invoegindex na verwijdering = oorspronkelijke bestemming
- aantal bronoffsets lager dan de bestemming
Bij bron {1} en bestemming 4 staat één verwijderd element oorspronkelijk vóór de grens. De verkleinde array is A, C, D, E, en de invoegindex is 4 - 1 = 3. Door B daar in te voegen, krijg je A, C, D, B, E.
Bij meerdere bronnen volstaat één correctie naar beneden niet. Bij bronnen {1, 3} en bestemming 5 zijn de verplaatste items B, D; de overblijvende array is A, C, E. Twee bronoffsets liggen vóór bestemming 5, dus de invoegindex is 3. Het resultaat is A, C, E, B, D.
Een eigen implementatie moet eerst de verplaatste elementen in hun oorspronkelijke volgorde vastleggen en daarna de originelen verwijderen zonder dat eerdere verwijderingen latere bronposities ongeldig maken. Vervolgens kan de aangepaste invoegpositie worden berekend en het vastgelegde blok in één keer worden ingevoegd. Voor deze arraygebaseerde lijst voorkom je met Apple’s bestaande methode dat je deze stappen zelf moet implementeren.
Welke randgevallen moet je testen?
De volgende gevallen zijn voorbeeldberekeningen voor de oorspronkelijke volgorde A, B, C, D, E. Zet die volgorde voor elk geval terug. De laatste kolom toont het verwachte modelresultaat, onafhankelijk van hoe de neerzetindicator eruitziet.
| Bronoffsets | Oorspronkelijke bestemming | Invoegindex na verwijdering | Verwachte volgorde |
|---|---|---|---|
{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 |
De twee gevallen zonder wijziging bij één item zijn van belang: een item invoegen op zijn eigen grens of op de daaropvolgende grens laat de volgorde ongewijzigd. Apple documenteert dit gedrag expliciet. Ook de eindgrens is geldig, hoewel je daarmee geen element kunt indexeren. Apple’s documentatie over move(fromOffsets:toOffset:).
Het rekenmodel achter deze zeven gevallen is gecontroleerd met een kleine deterministische Python-berekening. Het SwiftUI-programma is nagekeken, maar hier niet gecompileerd of uitgevoerd, omdat er geen Swift-toolchain en macOS-runtime beschikbaar waren. Controleer de daadwerkelijke sleepinteractie op de macOS-versie waarop je app draait.
Waarom verplaatsen rijen soms nog steeds verkeerd?
Begin bij de collectiegrens en controleer daarna de identiteit en de mutatie. Zo kun je problemen met bestemmingsberekeningen onderscheiden van verschillen tussen de weergegeven rijen en de opgeslagen gegevens.
- Controleer de collectie die aan
ForEachwordt doorgegeven. Als die een gefilterde array toont, verwijzen de offsets naar die gefilterde array. De zichtbare rijenB, Dhebben bijvoorbeeld offsets0, 1; bron0toepassen op de volledige array zouAverplaatsen. Dit gevolg vloeit voort uit Apple’s gedocumenteerde contract, waarin de callbackwaarden relatief zijn aan de collectie. Apple’s documentatie overonMove(perform:). - Controleer of de weergave de gegevens opnieuw sorteert. Als elke hertekening op label sorteert, kan het wijzigen van de volgorde in de backing-array geen blijvende handmatige weergavevolgorde opleveren. Bepaal het beleid voor handmatig sorteren voordat je herschikken in die weergave inschakelt.
- Controleer de identificaties. Behoud de identificatie van elk item wanneer je het verplaatst. Genereer geen identificaties opnieuw tijdens het renderen en gebruik veranderende posities niet als identiteit van verplaatsbare items. Apple’s documentatie over
ForEach. - Controleer op dubbele bewerkingen. Verwijder de handmatige aftrek van de bestemming en controleer daarna of de handler de verplaatsing één keer toepast op de gezaghebbende collectie. Een tweede verplaatsing werkt op een volgorde die al is gewijzigd.
Bepaal bij gefilterde interfaces of interfaces met secties eerst het gewenste gedrag voordat je indices omzet. Beslis of een verplaatsing alleen zichtbare items herschikt, de volledige collectie wijzigt of een item naar een andere sectie verplaatst. Koppel zichtbare items aan modelidentiteiten en bepaal de bestemming volgens dat beleid. Een numerieke offset alleen kan niet al deze keuzes uitdrukken.
Koppel callbackoffsets aan de momentopname van de collectie waarop ze betrekking hebben. Als de app de bewerking uitstelt terwijl andere wijzigingen de array aanpassen, bepaal dan opnieuw welke items en bestemming bedoeld zijn in plaats van verouderde posities opnieuw af te spelen. Het voorbeeld vermijdt die complicatie door de lokale array direct te wijzigen.
Veranderen AppKit of TextKit de bestemmingsregels?
Voor een app die al NSTableView gebruikt, heeft AppKit een eigen contract voor neerzetten. NSTableView.DropOperation.above betekent invoegen boven een rij, terwijl .on betekent dat je op die rij neerzet. Apple documenteert een neerzetactie onder de laatste rij als rij n met bewerking .above, waarbij n het aantal rijen is. Interpreteer zowel de rij als de bewerking voordat je een AppKit-neerzetactie omzet naar een modelverplaatsing. Apple’s documentatie over NSTableView.DropOperation.
TextKit gaat over tekstopslag, lay-out en bewerking via componenten zoals NSTextView en tekstlay-outbeheerders. Het levert niet het contract voor arrayherschikking dat in dit voorbeeld wordt gebruikt. Als een rij ook een editor bevat, houd de tekstbewerking gescheiden van de collectiehandeling die de rijvolgorde wijzigt. Apple’s documentatie over TextKit.
Houd voor een eenvoudige Mac-lijst één handmatig geordende collectie aan, behoud de identificaties van items en geef de argumenten van onMove rechtstreeks door aan de verplaatsmethode van de collectie. Gebruik de grensberekening en testmatrix wanneer je het resultaat moet diagnosticeren of de bewerking naar een ander model moet vertalen.
Bronnen
- onMove(perform:) — Apple
- move(fromOffsets:toOffset:) — Apple
- App — Apple
- ForEach — Apple
- State — Apple
- List — Apple
- EditButton — Apple
- NSTableView.DropOperation — Apple
- TextKit — Apple