Om du vill ordna om en lista i din Mac-app skickar du källan och destinationen från SwiftUIs onMove direkt till move(fromOffsets:toOffset:). Destinationen markerar en gräns i den ursprungliga samlingen, så om du manuellt subtraherar ett vid förflyttningar nedåt blir resultatet fel.
Den här genomgången bygger en liten inbyggd macOS-lista med syntetiska etiketter, förklarar destinationsberäkningen och ger en testmatris för att diagnostisera felplacerade rader. Omfattningen är att ordna om objekt i din egen apps lista.
Vad betyder onMove-destinationen?
Apple definierar de två argumenten för onMove(perform:) som positioner i förhållande till den underliggande samlingen för den dynamiska vyn. Det första argumentet är en IndexSet med källpositioner; det andra är destinationspositionen. Dessa positioner hör till samlingen som används av just den ForEach-vyn. Apples dokumentation om onMove(perform:).
För move(fromOffsets:toOffset:) definierar Apple destinationen som den position före vilken de flyttade elementen ska infogas. count får också användas för att ange slutet. Tänk på det som en gräns mellan element i den ursprungliga samlingen. Apples dokumentation om move(fromOffsets:toOffset:).
Betrakta den ursprungliga ordningen A, B, C, D, E. Elementens positioner är 0, 1, 2, 3, 4, medan insättningsgränserna löper från 0 till och med 5. Gränsen 4 ligger omedelbart före E; gränsen 5 ligger efter det sista objektet.
Om B flyttas från källposition 1 till destination 4 blir ordningen alltså A, C, D, B, E. Det flyttade objektet hamnar på index 3, trots att destinationsargumentet var 4. Båda värdena använder nollbaserade positioner, men beskriver olika steg i operationen.
Den skillnaden förklarar det vanliga felet vid förflyttning nedåt. Kod som ändrar destinationen 4 till 3 innan samlingens flyttmetod anropas begär en annan gräns och placerar B före D. Skicka callback-värdena oförändrade när du använder Apples flyttmetod.
Bygg en komplett omordningsbar macOS-lista
Använd ett inbyggt SwiftUI-approjekt för macOS i Xcode. Det fullständiga exemplet nedan har macOS 11 eller senare som mål eftersom det använder SwiftUIs App-livscykel; Apple anger att onMove i sig är tillgängligt från macOS 10.15. Ersätt de genererade deklarationerna för appen och innehållsvyn med den här koden, och se till att det bara finns en @main-ingångspunkt i målet. Apples dokumentation om App, Apples dokumentation om 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)
}
}
ForEach visar samma array som hanteraren ändrar. Varje objekt har en fast identifierare som inte beror på dess aktuella position. Apple kräver att element i ForEach kan identifieras eller att en uttrycklig identitetsnyckel anges. Om identifieraren bevaras vid en flytt kan vyn fortsätta representera samma objekt. Apples dokumentation om ForEach.
Arrayen är lokal vy-state. När den ändras uppdaterar SwiftUI beroende vyer, däribland listan och den visade ordningen. I det här exemplet lagras ordningen i minnet och ingen beständig lagring implementeras. Apples dokumentation om State.
Kör appen, dra B till gränsen omedelbart före E och granska både ordningen och callback-visningen. Använd Reset innan du testar nästa scenario. Den uttryckliga bindningen för markering stöder även markering av flera rader. Apple dokumenterar både markering med mängder och markering på Mac utan att gå in i redigeringsläge. Apples dokumentation om List.
Lägg inte till EditButton() i det här inbyggda macOS-exemplet. Apples aktuella tillgänglighetsinformation omfattar iOS och Mac Catalyst, men inte inbyggda macOS-appar. Kopiera inte redigeringskontroller från en mobilhandledning till ett Mac-mål utan att först kontrollera om de är tillgängliga. Apples dokumentation om EditButton.
Hur beräknar du det slutliga insättningsindexet?
Du behöver bara den här beräkningen om du själv implementerar borttagning och infogning, eller om du översätter flytten till en annan modelloperation. Den är en förklaringsmodell som bygger på Apples dokumenterade insättningssemantik, inte en extra justering som ska göras innan den inbyggda flyttmetoden anropas.
Beräkningen är:
insättningsindex efter borttagning = ursprunglig destination
- antal källpositioner före destinationen
För källan {1} och destinationen 4 ligger ett borttaget element ursprungligen före gränsen. Den reducerade arrayen är A, C, D, E, och insättningsindexet är 4 - 1 = 3. Om B infogas där blir resultatet A, C, D, B, E.
En enda korrigering nedåt räcker inte när flera källor flyttas. Med källpositionerna {1, 3} och destinationen 5 är de flyttade objekten B, D; den återstående arrayen är A, C, E. Två källpositioner ligger före destinationen 5, så insättningsindexet är 3. Resultatet blir A, C, E, B, D.
En egen implementation måste först fånga de flyttade elementen i deras ursprungliga ordning och sedan ta bort originalen utan att tidigare borttagningar gör senare källpositioner ogiltiga. Därefter kan den beräkna den justerade insättningspositionen och infoga det fångade blocket på en gång. För den här arraybaserade listan slipper du implementera stegen själv om du använder Apples befintliga metod.
Vilka specialfall bör du testa?
Följande fall är exempelberäkningar för den ursprungliga ordningen A, B, C, D, E. Återställ den ordningen före varje fall. Den sista kolumnen visar det förväntade resultatet enligt modellen, oberoende av hur släppindikatorn ser ut.
| Källpositioner | Ursprunglig destination | Insättningsindex efter borttagning | Förväntad ordning |
|---|---|---|---|
{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 två specialfallen där en flytt av ett enda objekt inte gör något är viktiga: om ett objekt infogas vid sin egen gräns eller gränsen direkt efter den, förblir ordningen oförändrad. Apple dokumenterar uttryckligen detta beteende. Även slutgränsen är giltig, trots att den inte kan användas för att hämta ett element med ett index. Apples dokumentation om move(fromOffsets:toOffset:).
Aritmetikmodellen bakom de här sju fallen kontrollerades med en liten deterministisk Python-beräkning. SwiftUI-programmet granskades, men kompilerades eller kördes inte här eftersom en Swift-verktygskedja och macOS-körmiljö saknades. Verifiera den faktiska dragningen på den macOS-version som är målet.
Varför kan rader fortfarande flyttas fel?
Börja med samlingsgränsen och kontrollera sedan identitet och ändringar. Den här ordningen skiljer problem med destinationsberäkningen från skillnader mellan raderna som visas och de lagrade uppgifterna.
- Kontrollera samlingen som skickas till
ForEach. Om vyn visar en filtrerad array beskriver positionerna den filtrerade arrayen. De synliga radernaB, Dhar till exempel positionerna0, 1; om källan0används på hela arrayen flyttasA. Följden bygger på Apples dokumenterade kontrakt om att callback-positioner är relativa till samlingen. Apples dokumentation omonMove(perform:). - Kontrollera om återrenderingen sorterar uppgifterna igen. Om varje omritning sorterar efter etikett kan en ändring av ordningen i den underliggande arrayen inte skapa en bestående manuell visningsordning. Bestäm en policy för manuell ordning innan du aktiverar omordning i vyn.
- Kontrollera identifierarna. Bevara varje objekts identifierare när det flyttas. Skapa inte nya identifierare vid återrendering och använd inte föränderliga positioner som identitet för objekt som kan flyttas. Apples dokumentation om
ForEach. - Kontrollera om samma sak görs två gånger. Ta bort manuell subtraktion från destinationen och bekräfta sedan att hanteraren utför flytten en gång på den auktoritativa samlingen. En andra flytt utgår från en ordning som redan har ändrats.
För filtrerade gränssnitt eller gränssnitt med sektioner måste du definiera önskat beteende innan du översätter index. Bestäm om en flytt ska ordna om endast synliga objekt, ändra hela samlingen eller flytta ett objekt mellan sektioner. Koppla synliga objekt till modellidentiteter och lös destinationen enligt den policyn. Ett numeriskt index kan inte ensamt uttrycka alla dessa val.
Låt callback-positionerna höra till den ögonblicksbild av samlingen som de beskriver. Om appen skjuter upp operationen medan andra redigeringar ändrar arrayen ska du lösa objekten och destinationen på nytt i stället för att återanvända inaktuella positioner. Exemplet slipper den komplikationen genom att ändra sin lokala array omedelbart.
Förändrar AppKit eller TextKit destinationsreglerna?
Om appen redan använder NSTableView har AppKit ett eget kontrakt för släpp. NSTableView.DropOperation.above betyder att ett objekt infogas ovanför en rad, medan .on betyder att det släpps på raden. Apple dokumenterar ett släpp under den sista raden som rad n med operationen .above, där n är antalet rader. Tolka både raden och operationen innan du översätter ett AppKit-släpp till en modellflytt. Apples dokumentation om NSTableView.DropOperation.
TextKit hanterar textlagring, layout och redigering genom komponenter som NSTextView och textlayoutshanterare. Det tillhandahåller inte kontraktet för omordning av arrayer som används i det här exemplet. Om en rad också innehåller en redigerare bör textredigeringen hållas åtskild från samlingsoperationen som ändrar radordningen. Apples dokumentation om TextKit.
För en enkel Mac-lista använder du en enda manuellt ordnad samling, bevarar objektens identifierare och skickar argumenten från onMove direkt till samlingens flyttmetod. Använd gränsberäkningen och testmatrisen när du behöver diagnostisera resultatet eller översätta operationen till en annan modell.
Källor
- onMove(perform:) — Apple
- move(fromOffsets:toOffset:) — Apple
- App — Apple
- ForEach — Apple
- State — Apple
- List — Apple
- EditButton — Apple
- NSTableView.DropOperation — Apple
- TextKit — Apple