Om du vill flytta en grupp och dess underordnade objekt tillsammans lagrar du de underordnade objekten i den överordnade modellen och ändrar ordningen på den överordnade samlingen. Den här guiden skapar en liten SwiftUI-lista för Mac där varje grupp flyttas som en rad, med de underordnade objekten fortfarande kopplade till den.
Uppgiften är att ordna om grupper i din egen app, till exempel genom att flytta Group A, som innehåller Tile A1 och Tile A2, under Group B. Exemplet använder syntetiska lokala data och har uttryckliga knappar för Move Up och Move Down tillsammans med SwiftUIs callback för flytt i listan.
Vad ska räknas som ett flyttbart objekt?
Låt flyttenheten motsvara enheten i dina data. För det här gränssnittet innehåller den yttre arrayen grupper, och varje grupp innehåller sin egen array med underordnade objekt. Om du ändrar ordning på den yttre arrayen ändras gruppordningen, samtidigt som varje grupp behåller sina underordnade objekt och deras befintliga ordning.
Representera varje grupp som en sammansatt listrad: en rubrik, flyttknappar och indragna etiketter för underordnade objekt i en VStack. Koppla onMove(perform:) till den yttre ForEach. Apple dokumenterar att callbackens offsetvärden refererar till den underliggande samlingen för den dynamiska vyn; här är den samlingen grupparrayen. Apples dokumentation om flyttcallbacken.
Ge grupper och underordnade objekt stabila identifierare. Apples ForEach kräver identifierade element, antingen via Identifiable eller en uttrycklig nyckelväg för identifieraren. I den här utformningen hör identifierarna till modellerna och förblir oförändrade när deras positioner ändras. Undvik att använda arraypositioner som identitet för innehåll som kan ordnas om. Apples dokumentation om ForEach.
En SwiftUI-vy av typen Group är inte applikationsmodellen som används nedan. Att namnge en behållare eller visa en rubrik anger inte vilka poster som ska flyttas tillsammans med den. Den överordnade modellens lagrade array children gör relationen tydlig.
Skapa ett komplett SwiftUI-exempel
Skapa ett macOS-approjekt i Xcode med SwiftUI-gränssnittet och macOS 12 eller senare som distributionsmål. Ersätt den genererade appens startfil med hela programmet nedan och behåll bara en @main-appdeklaration i målet. Exemplet använder medvetet en enkel lista och textknappar så att flytten är lätt att följa.
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)
}
}
Den yttre ForEach skapar en VStack per grupp. Den nästlade ForEach visar de underordnade objekten i den raden; det finns ingen separat hanterare för flytt av underordnade objekt. Båda flyttknapparna hittar gruppens aktuella position via ID och anropar samma ändring som används av onMove.
Kör appen och klicka på Move Down bredvid Group A. Den förväntade ordningen blir Group B, Group A, Group C, med Tile A1 och Tile A2 fortfarande visade under Group A. Om du klickar på Move Up bredvid gruppen återställs den ursprungliga ordningen. Group C visar också att en grupp utan underordnade objekt fortfarande kan flyttas.
Det här programmet behåller ordningen i minnet så länge den aktuella vyn finns; det har inget lager för beständig lagring. Referenserna och flyttlogiken har granskats, men programmet kompilerades inte här eftersom det tillgängliga skalet saknar Swift-kompilator. Bygg och kontrollera gränssnittet i Xcode på en Mac innan du integrerar det.
Varför används en annan destination vid flytt nedåt?
Destinationen som skickas till move(fromOffsets:toOffset:) är en infogningsgräns i den ursprungliga samlingen. Apple definierar den som offseten före vilken de flyttade elementen ska infogas, inom 0...count. Om du skickar ett enskilt elements egen offset eller den följande offseten ändras inget. Apples dokumentation om förflyttning i samlingar.
För den illustrativa startordningen [A, B, C] visar följande fristående operationer skillnaden:
| Avsett resultat | Källoffset | Destination som skickas till move |
Resultat |
|---|---|---|---|
Flytta A under B |
0 |
2 |
[B, A, C] |
Flytta B ovanför A |
1 |
0 |
[B, A, C] |
Flytta A till slutet |
0 |
3 |
[B, C, A] |
Låt A vara kvar |
0 |
1 |
[A, B, C] |
Därför använder knappen för flytt nedåt index + 2: den infogar före elementet efter nästa granne, eller i slutet. Knappen för flytt uppåt använder index - 1. För offsetvärden som skickas direkt från onMove skickar du dem vidare till samlingsmetoden utan att tillämpa den här knappen-specifika beräkningen igen.
Exemplets destinationsberäkning gäller en enskild intilliggande flytt. Om en flyttcallback innehåller flera källoffsetvärden använder du hela IndexSet med samlingsmetoden, som enligt dokumentationen bevarar ordningen på de flyttade elementen. Apples dokumentation om förflyttning i samlingar.
Vilket gränssnitt kräver en annan implementation?
Välj implementation efter hur gruppen visas och hur användarna behöver interagera med dess underordnade objekt. Exemplet med den sammansatta raden passar när en grupp alltid ska flyttas som ett block. Det visar medvetet underordnade objekt som etiketter inom blocket i stället för som fristående listrader.
| Gränssnittskrav | Lämplig utgångspunkt | Ansvar för flytten |
|---|---|---|
| Gruppens rubrik och underordnade objekt bildar ett flyttbart block | Yttre ForEach med en sammansatt rad per grupp |
Ändra ordningen på den överordnade arrayen |
| Expanderbar hierarki med nästlade underordnade objekt | OutlineGroup |
Implementera modellflytt för den avsedda överordnade samlingen |
| AppKit-samling med ett avsnitt per grupp | NSCollectionView |
Uppdatera datakällan och meddela sedan samlingsvyn om flytten av avsnittet |
| Anpassningsbar text i ett objekt | TextKit i objektets redigerare | Hantera gruppordningen i den omgivande samlingsmodellen |
Expanderbara träd kräver uttryckliga flyttregler
Apple beskriver OutlineGroup som en vy för identifierade, trädstrukturerade data med visningskontroller för att expandera och fälla ihop grenar. Den finns i macOS 11 och senare. Det dokumenterade presentationsbeteendet fastställer ingen generell policy för omordning av underträd i din app. Apples dokumentation om OutlineGroup.
För ett träd ska du definiera destinationen exakt: att flytta en överordnad nod bland sina syskon och att flytta den inuti en annan överordnad nod är olika operationer. En omordning bland syskon ändrar en överordnad array. Om en nod flyttas till en ny överordnad nod måste den tas bort från sin gamla förälder, infogas i den nya och en destination som ligger bland nodens egna underordnade objekt avvisas. Det här är regler för applikationsmodellen som följer av trädstrukturen.
Om uppgiften fortfarande är att flytta hela grupper på den översta nivån undviker implementationen med sammansatta rader behovet av en sådan ytterligare destinationspolicy. Om underordnade objekt behöver kunna väljas eller ordnas om separat ska du utforma de interaktionerna uttryckligen innan hierarkin plattas ut till synliga rader.
AppKit kan flytta ett avsnitt tillsammans med dess objekt
Om gränssnittet är en NSCollectionView där varje grupp representeras av ett avsnitt tillhandahåller Apple moveSection(_:toSection:). Dokumentationen omfattar uttryckligen omorganisering av avsnitt och deras innehållande objekt och kräver att datakällan uppdateras innan metoden anropas. API:et finns i macOS 10.11 och senare. Apples dokumentation om avsnittsflytt.
För den lösningen tar du fram källans och den slutliga destinationens avsnittsindex, ändrar ordningen på gruppdata och meddelar samlingsvyn med dess API för avsnittsflytt. Håll SwiftUIs beräkning av infogningsgränsen åtskild från det destinationsavsnittsindex som AppKit förväntar sig. Metoderna beskriver sina destinationer på olika sätt.
Om du vill bädda in en AppKit-samling i SwiftUI använder du NSViewRepresentable, Apples dokumenterade omslag för att skapa och uppdatera en AppKit-vy. Skicka tillbaka samlingsinteraktioner till SwiftUI-modellen via lämplig samordningsmekanism och låt SwiftUI styra den representerade vyns yttre layout. Apples dokumentation om NSViewRepresentable.
TextKit hanterar textlayout inuti ett objekt
Apple definierar NSTextLayoutManager som hanteraren för anpassad textlayout och presentation, tillgänglig i macOS 12 och senare. Dess ansvarsområden gäller textbehållare, element och layoutfragment. För den här samlingsuppgiften innebär det att gruppordningen ska hanteras i samlingsmodellen, även om ett underordnat objekt innehåller en anpassad TextKit-redigerare. Apples dokumentation om NSTextLayoutManager.
Hur felsöker du en grupp som flyttas fel?
Börja med det minsta synliga exemplet och granska modellen efter en enda operation. Kontrollerna nedan skiljer ett ordningsfel från en avvikelse mellan de visade raderna och de underliggande data.
- Bara rubriken flyttas: kontrollera om de underordnade objekten lagras som orelaterade poster i en platt array. Flytta föräldraobjektet som äger dem eller implementera uttryckligen en blockflytt för den platta representationen.
- Flytt nedåt gör ingenting: kontrollera om knappen skickar
index + 1. För ett enskilt källobjekt lämnar den gränsen objektet på samma plats. - Fel grupp flyttas efter filtrering: kontrollera om synliga offsetvärden används på hela arrayen. Slå upp synliga grupp-ID:n i den kanoniska modellen före ändringen och definiera var en filtrerad destination ska hamna.
- Ordningen återställs direkt: kontrollera om den visade arrayen sorteras på nytt vid varje rendering. Manuell ordning kräver en ordning som vyn faktiskt behåller.
- Ett underordnat objekt flyttas separat: kontrollera var flytthanteraren är kopplad. I det här exemplet hör den bara till den yttre gruppsamlingen.
Innan integrationen kontrollerar du flytt uppåt och nedåt, listans båda ändar, en enda återstående grupp och en tom grupp. Verifiera modellens invariants såväl som utseendet: samma grupp-ID:n finns kvar, varje grupp behåller samma ID:n för sina underordnade objekt och ordningen på de underordnade objekten förblir oförändrad. De här kontrollerna prövar direkt kravet på att föräldern och dess underordnade objekt ska flyttas tillsammans.
För ett kompakt Mac-gränssnitt börjar du med en överordnad modell per flyttbar rad och en gemensam omordningsfunktion. Användaren kan då flytta Group A med en enda åtgärd medan dess underordnade objekt stannar kvar. Lägg till en hierarkisk lösning eller AppKit-implementation när gränssnittet behöver oberoende interaktion med underordnade objekt eller avsnittsbaserat beteende i samlingen.
Källor
- onMove(perform:) — Apple
- ForEach — Apple
- move(fromOffsets:toOffset:) — Apple
- OutlineGroup — Apple
- moveSection(_:toSection:) — Apple
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple