Om een groep en de onderliggende items samen te verplaatsen, sla je de items op in het bovenliggende model en wijzig je de volgorde van de bovenliggende collectie. Deze handleiding bouwt een kleine SwiftUI Mac-lijst waarin elke groep als één rij wordt verplaatst, met de items er nog steeds aan gekoppeld.
De taak is groepen binnen je eigen app opnieuw ordenen: bijvoorbeeld Group A, met Tile A1 en Tile A2, onder Group B plaatsen. Het voorbeeld gebruikt synthetische lokale gegevens en biedt expliciete knoppen Move Up en Move Down, naast de callback voor het verplaatsen van de SwiftUI-lijst.
Wat moet tellen als één verplaatsbaar item?
Laat de verplaatsingseenheid overeenkomen met de eenheid in je gegevens. Voor deze interface bevat de buitenste array groepen, en elke groep bevat een eigen array met onderliggende items. Door die buitenste array opnieuw te ordenen, verandert de groepsvolgorde terwijl de items van elke groep en hun bestaande volgorde behouden blijven.
Stel elke groep voor als één samengestelde lijstregel: een kop, verplaatsingsknoppen en ingesprongen labels voor de onderliggende items in een VStack. Koppel onMove(perform:) aan de buitenste ForEach. Apple documenteert dat de offsets in de callback verwijzen naar de onderliggende collectie van die dynamische weergave; hier is dat de array met groepen. Apple's documentatie over de verplaatsingscallback.
Geef groepen en onderliggende items stabiele identificatiekenmerken. Apple vereist voor ForEach geïdentificeerde elementen, via Identifiable of een expliciet identificerend key path. In dit ontwerp horen de identificatiekenmerken bij de modellen en blijven ze ongewijzigd wanneer hun posities veranderen. Gebruik arrayposities niet als identiteit van inhoud die opnieuw kan worden geordend. Apple's ForEach-documentatie.
Een SwiftUI-weergave van het type Group is niet het toepassingsmodel dat hieronder wordt gebruikt. Een container een naam geven of een kop weergeven bepaalt niet welke onderliggende records ermee mee moeten verplaatsen. De opgeslagen array children van de bovenliggende groep maakt die relatie expliciet.
Bouw een volledig SwiftUI-voorbeeld
Maak in Xcode een macOS-appproject met de SwiftUI-interface en een implementatiedoel van macOS 12 of hoger. Vervang het gegenereerde app-invoerbestand door het volledige programma hieronder en zorg dat er in het doel maar één @main-appdeclaratie staat. Het voorbeeld gebruikt bewust een eenvoudige lijst en tekstknoppen, zodat je de verplaatsing gemakkelijk kunt bekijken.
import Foundation
import SwiftUI
private struct DemoItem: Identifiable {
let id: String
let title: String
}
private struct DemoGroup: Identifiable {
let id: String
let title: String
let children: [DemoItem]
}
@main
struct GroupMoveDemoApp: App {
var body: some Scene {
WindowGroup {
GroupCollectionView()
}
}
}
private struct GroupCollectionView: View {
@State private var groups: [DemoGroup] = [
DemoGroup(
id: "group-a", title: "Group A",
children: [
DemoItem(id: "a1", title: "Tile A1"),
DemoItem(id: "a2", title: "Tile A2")
]
),
DemoGroup(
id: "group-b", title: "Group B",
children: [
DemoItem(id: "b1", title: "Tile B1")
]
),
DemoGroup(
id: "group-c", title: "Group C",
children: []
)
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Arrange Groups")
.font(.title2)
.padding(.horizontal)
List {
ForEach(groups) { group in
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(group.title).font(.headline)
Spacer()
Button("Move Up") {
moveOne(id: group.id, down: false)
}
.disabled(group.id == groups.first?.id)
Button("Move Down") {
moveOne(id: group.id, down: true)
}
.disabled(group.id == groups.last?.id)
}
.buttonStyle(BorderlessButtonStyle())
ForEach(group.children) { child in
Text(child.title)
.padding(.leading, 16)
}
}
.padding(.vertical, 8)
}
.onMove(perform: moveGroups)
}
}
.padding(.vertical)
.frame(minWidth: 520, minHeight: 320)
}
private func moveGroups(from source: IndexSet, to destination: Int) {
withAnimation {
groups.move(fromOffsets: source, toOffset: destination)
}
}
private func moveOne(id: String, down: Bool) {
guard let index = groups.firstIndex(where: { $0.id == id }) else {
return
}
let neighbor = down ? index + 1 : index - 1
guard groups.indices.contains(neighbor) else {
return
}
let destination = down ? index + 2 : index - 1
moveGroups(from: IndexSet(integer: index), to: destination)
}
}
De buitenste ForEach maakt één VStack per groep. De geneste ForEach toont de onderliggende items binnen die rij; er is geen afzonderlijke handler voor het verplaatsen van items. Beide verplaatsingsknoppen zoeken de huidige positie van de groep op basis van ID en roepen dezelfde mutatie aan als onMove.
Start de app en klik op Move Down naast Group A. De verwachte volgorde wordt Group B, Group A, Group C, waarbij Tile A1 en Tile A2 nog steeds onder Group A worden weergegeven. Klik op Move Up naast die groep om de oorspronkelijke volgorde te herstellen. Group C laat ook zien dat een groep zonder onderliggende items verplaatsbaar blijft.
Dit programma bewaart de volgorde in het geheugen zolang de huidige weergave bestaat; het bevat geen persistentielaag. De verwijzingen en verplaatsingslogica zijn gecontroleerd, maar het programma is hier niet gecompileerd omdat de beschikbare shell geen Swift-compiler heeft. Bouw en controleer de interface in Xcode op een Mac voordat je deze integreert.
Waarom gebruikt omlaag verplaatsen een andere bestemming?
De bestemming die aan move(fromOffsets:toOffset:) wordt doorgegeven, is een invoeggrens in de oorspronkelijke collectie. Apple definieert dit als de offset vóór welke de verplaatste elementen moeten worden ingevoegd, binnen 0...count. De eigen offset van één element of de daaropvolgende offset doorgeven levert geen verandering op. Apple's documentatie over het verplaatsen van collecties.
Voor de illustratieve beginvolgorde [A, B, C] laten deze onafhankelijke bewerkingen het verschil zien:
| Gewenst resultaat | Bronoffset | Bestemming doorgegeven aan move |
Resultaat |
|---|---|---|---|
Verplaats A onder B |
0 |
2 |
[B, A, C] |
Verplaats B boven A |
1 |
0 |
[B, A, C] |
Verplaats A naar het einde |
0 |
3 |
[B, C, A] |
Laat A staan |
0 |
1 |
[A, B, C] |
Daarom gebruikt de omlaagknop index + 2: zo wordt het item ingevoegd vóór het item na de volgende buur, of aan het einde. De omhoogknop gebruikt index - 1. Geef offsets die rechtstreeks door onMove worden aangeleverd door aan de collectiemethode zonder deze knopsspecifieke berekening nogmaals toe te passen.
De bestemmingsberekening in het voorbeeld geldt voor één aangrenzende verplaatsing. Gebruik bij een verplaatsingscallback met meerdere bronoffsets de volledige IndexSet met de collectiemethode, die aangeeft dat de volgorde van de verplaatste elementen behouden blijft. Apple's documentatie over het verplaatsen van collecties.
Welke interface vereist een andere implementatie?
Kies de implementatie op basis van hoe de groep wordt weergegeven en hoe gebruikers met de onderliggende items moeten kunnen werken. Het voorbeeld met samengestelde rijen is geschikt wanneer een groep altijd als één blok moet worden verplaatst. De onderliggende items worden bewust als labels binnen dat blok weergegeven, in plaats van als afzonderlijke lijstregels.
| Interfacevereiste | Geschikt uitgangspunt | Verantwoordelijkheid voor verplaatsen |
|---|---|---|
| Groepskop en onderliggende items vormen één verplaatsbaar blok | Buitenste ForEach met één samengestelde rij per groep |
De bovenliggende array opnieuw ordenen |
| Uitklapbare hiërarchie met geneste afstammelingen | OutlineGroup |
Verplaatsingslogica voor de bedoelde bovenliggende collectie implementeren |
| AppKit-collectie met één sectie per groep | NSCollectionView |
De gegevensbron bijwerken en de collectie weergave vervolgens op de hoogte stellen van de sectieverplaatsing |
| Aangepaste bewerkbare tekst binnen een item | TextKit binnen de editor van het item | Het opnieuw ordenen van groepen in het omringende collectiemodel houden |
Voor uitklapbare bomen zijn expliciete verplaatsingsregels nodig
Apple beschrijft OutlineGroup als een weergave van geïdentificeerde, boomgestructureerde gegevens met bedieningselementen om vertakkingen uit en in te klappen. Deze is beschikbaar in macOS 11 en hoger. De gedocumenteerde weergavewerking legt geen algemeen beleid vast voor het opnieuw ordenen van subbomen in je toepassing. Apple's OutlineGroup-documentatie.
Bepaal voor een boom de bestemming nauwkeurig: een bovenliggend element tussen zijn broers en zussen verplaatsen is een andere bewerking dan het binnen een ander bovenliggend element plaatsen. Een volgordewijziging tussen broers en zussen verandert één bovenliggende array. Een element opnieuw onder een ander element plaatsen vereist dat je het knooppunt bij de oude bovenliggende groep verwijdert, het bij de nieuwe invoegt en een bestemming binnen de eigen afstammelingen van dat knooppunt afwijst. Dit zijn regels voor het toepassingsmodel die voortvloeien uit de boomstructuur.
Als de taak beperkt blijft tot het verplaatsen van volledige groepen op het hoogste niveau, vermijdt de implementatie met samengestelde rijen dit extra bestemmingsbeleid. Als onderliggende items afzonderlijk moeten kunnen worden geselecteerd of verplaatst, ontwerp die interacties dan expliciet voordat je de hiërarchie omzet in zichtbare rijen.
AppKit kan een sectie met de bijbehorende items verplaatsen
Als je interface een NSCollectionView is waarin elke groep als sectie wordt weergegeven, biedt Apple moveSection(_:toSection:). De documentatie gaat expliciet in op het opnieuw indelen van secties en de bijbehorende items en vereist dat je de gegevensbron bijwerkt voordat je deze aanroept. De API is beschikbaar in macOS 10.11 en hoger. Apple's documentatie over sectieverplaatsing.
Voor die aanpak bepaal je de bronindex van de sectie en de uiteindelijke bestemmingsindex, orden je de groepsgegevens opnieuw en stel je de collectie weergave met de API voor sectieverplaatsing daarvan op de hoogte. Houd de berekening van de invoeggrens in SwiftUI gescheiden van de bestemmingssectie-index die AppKit verwacht. De twee methoden beschrijven hun bestemmingen op verschillende manieren.
Gebruik NSViewRepresentable om een AppKit-collectie in SwiftUI in te sluiten; Apple documenteert dit als de wrapper om een AppKit-weergave te maken en bij te werken. Koppel collectie-interacties via het daarvoor geschikte coördinatiemechanisme terug aan het SwiftUI-model en laat SwiftUI de buitenste lay-out van de weergegeven weergave bepalen. Apple's NSViewRepresentable-documentatie.
TextKit verzorgt de tekstindeling binnen een item
Apple definieert NSTextLayoutManager als de manager voor aangepaste tekstindeling en -weergave, beschikbaar in macOS 12 en hoger. De verantwoordelijkheden ervan betreffen tekstcontainers, elementen en lay-outfragmenten. Voor deze collectietaak betekent dit architectonisch dat de volgorde van groepen in het collectiemodel blijft, zelfs als een onderliggend item een aangepaste TextKit-editor bevat. Apple's NSTextLayoutManager-documentatie.
Hoe spoor je op waarom een groep verkeerd wordt verplaatst?
Begin met het kleinst zichtbare voorbeeld en bekijk het model na één bewerking. Met de onderstaande controles kun je een volgordefout onderscheiden van een verschil tussen de weergegeven rijen en de onderliggende gegevens.
- Alleen de kop verplaatst: controleer of de onderliggende items als losse elementen in een platte array zijn opgeslagen. Verplaats het bovenliggende object waartoe ze behoren, of implementeer expliciet een blokverplaatsing voor de platte weergave.
- Omlaag verplaatsen doet niets: controleer of de knop
index + 1doorgeeft. Voor één bronitem laat die grens het item op zijn plaats. - Na filteren verplaatst de verkeerde groep: controleer of zichtbare offsets op de volledige array worden toegepast. Koppel zichtbare groeps-ID's vóór de mutatie aan het canonieke model en bepaal waar een gefilterde bestemming thuishoort.
- De volgorde wordt meteen teruggedraaid: controleer of de weergegeven array bij elke render opnieuw wordt gesorteerd. Voor handmatige ordening is een volgorde nodig die in de weergave behouden blijft.
- Een onderliggend item verplaatst zelfstandig: controleer waaraan de verplaatsingshandler is gekoppeld. In dit voorbeeld hoort deze alleen bij de buitenste groepscollectie.
Controleer vóór integratie het verplaatsen omhoog en omlaag, beide uiteinden van de lijst, een lijst met nog maar één groep en een lege groep. Controleer zowel de invarianten van het model als de weergave: dezelfde groeps-ID's blijven aanwezig, elke groep behoudt dezelfde ID's van onderliggende items en hun volgorde blijft ongewijzigd. Deze controles toetsen rechtstreeks of de bovenliggende groep en de onderliggende items samen worden verplaatst.
Begin voor een compacte Mac-interface met één bovenliggend model per verplaatsbare rij en één gedeelde functie om de volgorde te wijzigen. De gebruiker kan Group A dan met één handeling verplaatsen terwijl de onderliggende items eraan gekoppeld blijven. Voeg een hiërarchische of AppKit-implementatie toe wanneer de interface afzonderlijke interacties met onderliggende items of sectiegebonden collectiegedrag vereist.
Bronnen
- onMove(perform:) — Apple
- ForEach — Apple
- move(fromOffsets:toOffset:) — Apple
- OutlineGroup — Apple
- moveSection(_:toSection:) — Apple
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple