Per spostare un gruppo e i suoi elementi figlio insieme, memorizza i figli all'interno del modello genitore e riordina la raccolta genitore. Questa guida crea un piccolo elenco Mac SwiftUI in cui ogni gruppo si sposta come una singola riga, con i suoi figli ancora attaccati.

L'attività consiste nel riordinare i gruppi all'interno della propria app: ad esempio, spostare Gruppo A, contenente Riquadro A1 e Riquadro A2, sotto Gruppo B. L'esempio utilizza dati locali sintetici e fornisce espliciti pulsanti Sposta su e Sposta giù accanto al callback di spostamento dell'elenco SwiftUI.

Cosa dovrebbe contare come un singolo elemento spostabile?

Fai coincidere l'unità di movimento con l'unità nei tuoi dati. Per questa interfaccia, l'array esterno contiene i gruppi e ogni gruppo contiene il proprio array di figli. Riordinare quell'array esterno cambia l'ordine dei gruppi mantenendo i figli di ogni gruppo e il loro ordine esistente.

Rappresenta ogni gruppo come una singola riga di elenco composita: un'intestazione, pulsanti di movimento ed etichette figlie indentate all'interno di un VStack. Collega onMove(perform:) all'ForEach esterno. Apple documenta che gli offset del callback si riferiscono alla raccolta sottostante di quella vista dinamica; qui, quella raccolta è l'array dei gruppi. Documentazione sul callback di spostamento di Apple.

Assegna a gruppi e figli identificatori stabili. L'ForEach di Apple richiede elementi identificati, tramite Identifiable o un percorso di chiave identificativo esplicito. In questo design, gli identificatori appartengono ai modelli e rimangono invariati quando cambiano le loro posizioni. Evita di usare le posizioni degli array come identità del contenuto riordinabile. Documentazione su ForEach di Apple.

Una vista Group di SwiftUI non è il modello applicativo utilizzato di seguito. Nominare un contenitore o visualizzare un'intestazione non stabilisce quali record figli debbano spostarsi con esso. L'array children memorizzato nel genitore rende esplicita quella relazione.

Crea un esempio SwiftUI completo

Crea un progetto app macOS in Xcode con interfaccia SwiftUI e target di deployment macOS 12 o successivo. Sostituisci il file di ingresso dell'app generato con il programma completo seguente, mantenendo una sola dichiarazione @main dell'app nel target. L'esempio usa deliberatamente un elenco semplice e pulsanti di testo per rendere lo spostamento facile da ispezionare.

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)
    }
}

L'ForEach esterno produce un VStack per gruppo. Il suo ForEach nidificato visualizza i figli all'interno di quella riga; non c'è un gestore di movimento separato per i figli. Entrambi i pulsanti di movimento trovano la posizione corrente del gruppo tramite ID e chiamano la stessa mutazione usata da onMove.

Esegui l'app e clicca Sposta giù accanto a Gruppo A. L'ordine atteso diventa Gruppo B, Gruppo A, Gruppo C, con Riquadro A1 e Riquadro A2 ancora visualizzati sotto Gruppo A. Cliccando Sposta su accanto a quel gruppo si ripristina l'ordine iniziale. Gruppo C dimostra anche che un gruppo senza figli rimane spostabile.

Questo programma mantiene l'ordine in memoria per la durata della vista corrente; non contiene alcun livello di persistenza. I suoi riferimenti e la logica di movimento sono stati ispezionati, ma non è stato compilato qui perché la shell disponibile non ha un compilatore Swift. Esegui il build e verifica l'interfaccia in Xcode su un Mac prima di integrarla.

Perché lo spostamento verso il basso usa una destinazione diversa?

La destinazione passata a move(fromOffsets:toOffset:) è un confine di inserimento nella raccolta originale. Apple la definisce come l'offset prima del quale inserire gli elementi spostati, all'interno di 0...count. Passare l'offset dello stesso elemento o l'offset successivo non produce alcun cambiamento. Documentazione sul movimento delle raccolte di Apple.

Per l'ordine iniziale illustrativo [A, B, C], queste operazioni indipendenti mostrano la distinzione:

Risultato inteso Offset sorgente Destinazione passata a move Risultato
Sposta A sotto B 0 2 [B, A, C]
Sposta B sopra A 1 0 [B, A, C]
Sposta A alla fine 0 3 [B, C, A]
Lascia A al suo posto 0 1 [A, B, C]

Ecco perché il pulsante verso il basso usa index + 2: inserisce prima dell'elemento dopo il vicino successivo, o alla fine. Il pulsante verso l'alto usa index - 1. Per gli offset forniti direttamente da onMove, passali al metodo della raccolta senza applicare di nuovo questo calcolo specifico per i pulsanti.

L'aritmetica della destinazione nell'esempio si applica a uno spostamento adiacente singolo. Per un callback di movimento contenente più offset sorgente, usa l'IndexSet completo con il metodo della raccolta, che documenta la preservazione dell'ordinamento degli elementi spostati. Documentazione sul movimento delle raccolte di Apple.

Quale interfaccia necessita di un'implementazione diversa?

Scegli l'implementazione in base a come appare il gruppo e a come gli utenti devono interagire con i suoi figli. L'esempio di riga composita è una soluzione utile quando un gruppo deve sempre spostarsi come un unico blocco. Fornisce deliberatamente etichette dei figli all'interno di quel blocco piuttosto che righe di elenco indipendenti.

Requisito interfaccia Punto di partenza adatto Responsabilità movimento
Intestazione gruppo e figli formano un blocco spostabile unico ForEach esterno con una riga composita per gruppo Riordina l'array genitore
Gerarchia espandibile con discendenti nidificati OutlineGroup Implementa il movimento del modello per la raccolta genitore prevista
Raccolta AppKit con una sezione per gruppo NSCollectionView Aggiorna la sorgente dati, poi notifica alla collection view lo spostamento della sezione
Testo modificabile personalizzato all'interno di un elemento TextKit all'interno dell'editor dell'elemento Mantieni il riordino dei gruppi nel modello della raccolta circostante

Gli alberi espandibili necessitano regole di movimento esplicite

Apple descrive OutlineGroup come una vista di dati ad albero identificati con controlli di disclosure per espandere e comprimere i rami. È disponibile su macOS 11 e versioni successive. Il suo comportamento di presentazione documentato non stabilisce una politica generale di riordino dei sottalberi per la tua applicazione. Documentazione su OutlineGroup di Apple.

Per un albero, definisci la destinazione con precisione: spostare un genitore tra i suoi fratelli e spostarlo dentro un altro genitore sono operazioni diverse. Un riordino tra fratelli cambia un array genitore. Il riparenting richiede la rimozione del nodo dal suo vecchio genitore, l'inserimento nel nuovo genitore e il rifiuto di una destinazione all'interno dei discendenti di quel nodo. Queste sono regole del modello applicativo derivate dalla struttura ad albero.

Se l'attività rimane lo spostamento di interi gruppi di primo livello, l'implementazione a riga composita evita quella politica di destinazione aggiuntiva. Se i figli necessitano di selezione o riordino indipendenti, progetta esplicitamente quelle interazioni prima di appiattire la gerarchia in righe visibili.

AppKit supporta lo spostamento di una sezione con i suoi elementi contenuti

Quando la tua interfaccia è un NSCollectionView con ogni gruppo rappresentato da una sezione, Apple fornisce moveSection(_:toSection:). La sua documentazione copre esplicitamente la riorganizzazione delle sezioni e dei loro elementi contenuti e richiede l'aggiornamento della sorgente dati prima di chiamarla. L'API è disponibile su macOS 10.11 e versioni successive. Documentazione sul movimento delle sezioni di Apple.

Per quel percorso, risolvi gli indici di sezione sorgente e destinazione finali, riordina i dati dei gruppi e notifica alla collection view usando la sua API di movimento delle sezioni. Mantieni separato il calcolo del confine di inserimento di SwiftUI dall'indice di sezione destinazione atteso da AppKit. I due metodi descrivono le loro destinazioni in modo diverso.

Per incorporare una raccolta AppKit in SwiftUI, usa NSViewRepresentable, il wrapper documentato di Apple per creare e aggiornare una vista AppKit. Inoltra le interazioni della raccolta al modello SwiftUI tramite il meccanismo di coordinamento appropriato e lascia che SwiftUI controlli il layout esterno della vista rappresentata. Documentazione su NSViewRepresentable di Apple.

TextKit gestisce il layout del testo all'interno di un elemento

Apple definisce NSTextLayoutManager come il gestore per il layout e la presentazione personalizzati del testo, disponibile su macOS 12 e versioni successive. Le sue responsabilità riguardano i contenitori di testo, gli elementi e i frammenti di layout. Per questo compito di raccolta, la conseguenza architetturale è mantenere l'ordinamento dei gruppi nel modello della raccolta, anche se un figlio contiene un editor TextKit personalizzato. Documentazione su NSTextLayoutManager di Apple.

Come diagnosticare un gruppo che si sposta in modo errato?

Inizia con l'esempio visibile più piccolo e ispeziona il modello dopo un'operazione. I controlli seguenti distinguono un errore di ordinamento da un disallineamento tra le righe visualizzate e i dati sottostanti.

  1. Si sposta solo l'intestazione: verifica se i figli sono memorizzati come voci non correlate in un array piatto. Sposta l'oggetto genitore che li possiede, oppure implementa esplicitamente uno spostamento a blocchi per la rappresentazione piatta.
  2. Lo spostamento verso il basso non fa nulla: verifica se il pulsante passa index + 1. Per un singolo elemento sorgente, quel confine lascia l'elemento al suo posto.
  3. Il gruppo sbagliato si sposta dopo il filtraggio: verifica se gli offset visibili vengono applicati all'array completo. Risolvi gli ID dei gruppi visibili nel modello canonico prima della mutazione e definisci dove appartiene una destinazione filtrata.
  4. L'ordine si ripristina immediatamente: verifica se l'array visualizzato viene riordinato a ogni rendering. L'ordinamento manuale necessita di un ordine che la visualizzazione mantenga effettivamente.
  5. Un figlio si sposta indipendentemente: verifica dove è collegato il gestore di movimento. In questo esempio, appartiene solo alla raccolta esterna dei gruppi.

Prima dell'integrazione, controlla il movimento verso l'alto e verso il basso, entrambe le estremità dell'elenco, un singolo gruppo rimanente e un gruppo vuoto. Verifica gli invarianti del modello oltre all'aspetto: gli stessi ID di gruppo rimangono presenti, ogni gruppo mantiene gli stessi ID figli e l'ordine dei figli rimane invariato. Questi controlli testano direttamente il requisito che il genitore e i suoi figli viaggino insieme.

Per un'interfaccia Mac compatta, inizia con un modello genitore per riga spostabile e una funzione di riordino condivisa. L'utente può quindi spostare Gruppo A con un'unica azione mentre i suoi figli rimangono attaccati. Aggiungi un'implementazione gerarchica o AppKit quando l'interfaccia necessita di interazione indipendente con i figli o comportamento di raccolta basato su sezioni.

Fonti