Skip to content
Alle Artikel

SwiftUI Liste sortieren in iOS 27: reorderable() und Swipe Actions

Listen, Stacks und Grids in SwiftUI unter iOS 27 per Drag sortieren mit reorderable() und reorderContainer, plus Swipe Actions ohne List, auch auf dem iPhone Duo.

Von der AppleDuo-RedaktionVeröffentlicht 7 Min. Lesezeit

Auch verfügbar auf:EnglishEspañolFrançais日本語Türkçe

Auf dieser Seite
  1. Wie sortiere ich eine SwiftUI-Liste in iOS 27?
  2. Wie aktualisiert die Reorder-Closure meine Daten?
  3. Worin unterscheidet sich reorderable() von onMove und EditButton?
  4. Kann ich in iOS 27 Swipe Actions außerhalb einer List verwenden?
  5. Sind diese APIs auf älteren iOS-Versionen verfügbar?
  6. Wie hilft das auf dem iPhone Duo?
  7. Die wichtigsten Punkte

Jahrelang lautete die Antwort auf „Wie lasse ich Nutzer eine SwiftUI-Liste sortieren?“ immer gleich: .onMove an ein ForEach in einer List hängen, einen EditButton hinzufügen und Index-Offsets zurück in das eigene Modell übersetzen. Das funktionierte nur in List und brachte den Bearbeitungsmodus mit. Dieser Leitfaden zeigt, wie Sie eine SwiftUI-Liste in iOS 27 sortieren, mit einer API, die in jedem Container funktioniert und beides nicht braucht.

iOS 27 hebt außerdem eine zweite Beschränkung auf List auf: Swipe Actions. Beide Änderungen stammen von der WWDC26 und sind in Apples SwiftUI-Referenz dokumentiert. Der Beitrag basiert auf dieser Dokumentation, Apples Session Code-along: Build powerful drag and drop in SwiftUI und Beiträgen des iOS-Entwicklers @sarunw. Diese APIs sind neu in iOS 27.0, und Details können sich zwischen Releases ändern.

Wie sortiere ich eine SwiftUI-Liste in iOS 27?

Sie brauchen zwei Modifier. @sarunw fasst es so zusammen: iOS 27 gibt ForEach ein eigenes Drag-to-Reorder. Markieren Sie es mit .reorderable() und setzen Sie .reorderContainer auf die List. Kein .onMove, kein Bearbeitungsmodus.

  • reorderable() kommt an das ForEach (jeden DynamicViewContent), dessen Views Nutzer verschieben können.
  • reorderContainer(for:isEnabled:move:) kommt an den Container und legt den Bereich des Drags fest. Sie übergeben Ihren Elementtyp und eine move-Closure, die die Änderung erhält.
struct TaskListView: View {
    @State private var tasks: [TaskItem] = TaskItem.samples

    var body: some View {
        List {
            ForEach(tasks) { task in
                TaskRow(task: task)
            }
            .reorderable()
        }
        .reorderContainer(for: TaskItem.self) { difference in
            tasks.apply(difference)
        }
    }
}

TaskItem ist Identifiable, und Apples Leitfaden verlangt einen ID-Typ, der Hashable und Sendable ist. Der Aufruf apply(_:) ist unser eigener Helper, den der nächste Abschnitt zeigt; SwiftUI dokumentiert keinen für Arrays.

Apple beschreibt die Interaktion selbst: Eine Person hebt eine View mit einer Drag-Geste an, das System setzt einen Platzhalter dorthin, wo sie abgelegt werden kann, der Platzhalter folgt dem Drag, und beim Ablegen ruft das System move auf. Sie schreiben weder Gesten-Code noch Animationen.

Der Container muss keine List sein. Laut Apples Dokumentation kann er eine Liste, ein Stack, ein Grid oder ein eigenes Layout sein. Apples eigenes Beispiel wendet .reorderContainer auf einen einfachen VStack an, und die WWDC26-Session verwendet einen HStack aus Spielkarten.

Wie aktualisiert die Reorder-Closure meine Daten?

Die move-Closure erhält eine ReorderDifference. Sie beschreibt die Änderung über die Identität, nicht über den Index:

EigenschaftTypBedeutung
sources[ItemID]IDs der verschobenen Elemente, in der Reihenfolge, in der die Person sie ausgewählt hat
destination.position.before(_:) oder .endVor dem Element mit dieser ID einfügen oder ans Ende anhängen
destination.collectionIDIhr Collection-ID-TypWelche Section die Elemente erhält (Container mit mehreren Collections)

Die Arbeit mit IDs hat einen praktischen Vorteil. Apples Leitfaden merkt an, dass Sie die Elemente verschieben können, selbst wenn gleichzeitig andere Änderungen am Array passieren, etwa eine Cloud-Synchronisierung, weil eine ID auch nach einer Änderung des Arrays auf das richtige Element zeigt. Ein IndexSet aus .onMove tut das nicht.

So lässt sich eine Difference mit einer einzelnen Collection auf ein Array anwenden. Der Helper entfernt die verschobenen Elemente und fügt sie in Auswahlreihenfolge am Ziel wieder ein:

@available(iOS 27.0, *)
extension Array where Element: Identifiable, Element.ID: Sendable {
    mutating func apply(
        _ difference: ReorderDifference<Element.ID, ReorderableSingleCollectionIdentifier>
    ) {
        let moved = difference.sources.compactMap { id in
            first { $0.id == id }
        }
        removeAll { difference.sources.contains($0.id) }

        switch difference.destination.position {
        case .before(let id):
            let index = firstIndex { $0.id == id } ?? endIndex
            insert(contentsOf: moved, at: index)
        case .end:
            append(contentsOf: moved)
        }
    }
}

Der generische Typ entspricht der Überladung für eine einzelne Collection, deren Closure ReorderDifference<Item.ID, ReorderableSingleCollectionIdentifier> erhält. Betrachten Sie den Helper als Ausgangspunkt und testen Sie ihn mit Ihrem Modell.

Sortieren über Sections hinweg

Für Daten in Sections geben Sie jedem ForEach mit reorderable(collectionID:) eine Kennung und verwenden reorderContainer(for:in:isEnabled:move:) mit dem Typ, der Sections identifiziert. Nutzer können Elemente dann innerhalb von und zwischen Sections verschieben, und destination.collectionID sagt Ihnen, welche Section sie erhalten hat.

List {
    ForEach(model.sections) { section in
        Section(section.name) {
            ForEach(section.reminders) { reminder in
                ReminderView(reminder)
            }
            .reorderable(collectionID: section.id)
        }
    }
}
.reorderContainer(for: Reminder.self, in: ReminderModel.Section.ID.self) { difference in
    model.apply(difference: difference)
}

Das entspricht Apples Beispiel aus der Dokumentation; model.apply(difference:) ist der Code Ihres Modells. Wenn Ihr ForEach eine andere ID als id verwendet, nehmen die itemID:-Überladungen einen Key Path entgegen, etwa \.databaseID.

Worin unterscheidet sich reorderable() von onMove und EditButton?

Das ältere Muster funktioniert weiterhin. Vor einer Migration lohnt es sich, genau zu wissen, was sich ändert:

.onMove + EditButton.reorderable() + .reorderContainer
EingeführtiOS 13iOS 27.0
ContainerListListe, Stack, Grid oder eigenes Layout
Wie es startetZiehgriffe, meist im BearbeitungsmodusDrag-Geste des Systems
Eingabe der Closure(IndexSet, Int)-OffsetsReorderDifference mit Element-IDs
Verschieben zwischen SectionsJedes ForEach hat sein eigenes onMovereorderable(collectionID:), eine Closure
Mehrere ElementeIn der Dokumentation nicht beschriebenQuellen in Auswahlreihenfolge
Deaktivierennil an onMove übergebenisEnabled: false
Hineinziehen aus anderen AppsSeparates onInsert oder dropDestinationKombination mit dropDestination und reorderDestination

Der langjährige Leitfaden von Hacking with Swift zeigt das klassische Setup: onMove(perform:) am ForEach und einen EditButton in der Toolbar, damit die Ziehgriffe erscheinen. Apples Dokumentation zu onMove beschreibt die Argumente der Closure als Offsets in der zugrunde liegenden Collection, weshalb der alte Code meist mit move(fromOffsets:toOffset:) endet.

Der Parameter isEnabled ist in echten Apps praktisch. Apples Leitfaden schlägt vor, das Sortieren während einer Synchronisierung zu deaktivieren, zum Beispiel mit isEnabled: !model.isSaving.

Die Session ergänzt einen Design-Tipp aus ihrem Solitaire-Beispiel: Um manche Elemente fest zu halten, teilen Sie sie in ein zweites ForEach ohne .reorderable() auf. Nur markierter Inhalt lässt sich anheben. Apples Dokumentation beschreibt nicht, .onMove und .reorderable() am selben ForEach zu kombinieren, entscheiden Sie sich also für eines.

Kann ich in iOS 27 Swipe Actions außerhalb einer List verwenden?

Ja. @sarunw bringt es auf den Punkt: Swipe Actions brauchten früher eine List-Zeile, in iOS 27 kann jede View sie haben. Möglich macht das swipeActionsContainer().

Sie schreiben .swipeActions weiterhin an jede Zeile, genau wie in einer List. Dann wenden Sie .swipeActionsContainer() auf die ScrollView, den LazyVStack oder einen anderen Container an, der die Zeilen enthält:

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item)
                .swipeActions {
                    Button("Delete", role: .destructive) {
                        delete(item)
                    }
                }
        }
    }
}
.swipeActionsContainer()

Apples Dokumentation listet auf, was der Container koordiniert:

  • Es werden immer nur die Swipe Actions einer Zeile angezeigt.
  • Scrollen im Container schließt geöffnete Aktionen.
  • Tippen außerhalb der aktiven Zeile schließt ihre Aktionen.

List erledigt all das bereits, daher bezeichnet Apple den Modifier an einer List als wirkungslos. Majid Jabrayilovs Artikel kommt zur selben Regel: Alles außer List braucht den Container-Modifier.

iOS 27 ergänzt außerdem swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:). Seine Closure erhält true, wenn die Aktionen einer Zeile erscheinen, und false, wenn sie geschlossen werden. Apple schlägt vor, damit die Zeile abzudunkeln oder umgebende UI zu aktualisieren, während Aktionen sichtbar sind.

Bekannte Probleme, die Sie prüfen sollten

Zwei Entwicklerberichte in Apples Forums sollten Sie mit Ihrem eigenen Build abgleichen. Einer besagt, dass in Rechts-nach-links-Layouts die Aktionen an der gespiegelten Kante erscheinen, die Geste aber weiterhin die Links-nach-rechts-Richtung erwartet. Der andere besagt, dass der Inhalt der Swipe Actions außerhalb von List bei Zustandsänderungen nicht neu ausgewertet wird, sodass ein Titel „Pin“ womöglich erst zu „Unpin“ wechselt, wenn die Zeile neu erstellt wird. Zum Zeitpunkt der Veröffentlichung gab es in keinem der Threads eine Antwort von Apple.

Sind diese APIs auf älteren iOS-Versionen verfügbar?

Nein. reorderable(), jede reorderContainer-Überladung, swipeActionsContainer() und die Überladung mit onPresentationChanged sind als verfügbar ab iOS 27.0, iPadOS 27.0, Mac Catalyst 27.0, macOS 27.0, visionOS 27.0 und watchOS 27.0 dokumentiert. Die WWDC26-Session beschreibt die Sortier-APIs als neu verfügbar auf allen Apple-Plattformen, die Drag and Drop unterstützen.

Wenn Sie iOS 26 oder älter unterstützen, verzweigen Sie mit #available und behalten den alten Weg bei:

@ViewBuilder
private var taskList: some View {
    if #available(iOS 27.0, *) {
        List {
            ForEach(tasks) { TaskRow(task: $0) }
                .reorderable()
        }
        .reorderContainer(for: TaskItem.self) { tasks.apply($0) }
    } else {
        List {
            ForEach(tasks) { TaskRow(task: $0) }
                .onMove { tasks.move(fromOffsets: $0, toOffset: $1) }
        }
        .toolbar { EditButton() }
    }
}

Für Swipe Actions außerhalb von List gibt es auf älteren Versionen keinen Fallback; Nil Coalescing merkt an, dass sie vor iOS 27 an Zeilen in einem LazyVStack oder Grid keine Wirkung hatten. Behalten Sie diese Zeilen dort in einer List oder bieten Sie dieselben Aktionen in einem Kontextmenü an.

Wie hilft das auf dem iPhone Duo?

Auf dem iPhone Duo meldet das innere Display eine reguläre horizontale Size Class, wie ein iPad, während das äußere Display compact bleibt (siehe unseren Leitfaden zu Size Classes auf dem iPhone Duo). Viele Apps nutzen die zusätzliche Breite für ein Grid oder ein eigenes Layout statt einer einspaltigen List.

Bis iOS 27 kostete diese Entscheidung Funktionen. Ein LazyVGrid aus Karten hatte weder eingebautes Sortieren noch Swipe Actions, der Wechsel zu einem Grid auf dem inneren Display bedeutete also, beides von Hand nachzubauen. Jetzt funktioniert derselbe Code mit .reorderable() und .swipeActions, egal ob ein Wechsel der Size Class auf dem äußeren Display eine List und auf dem inneren ein Grid zeigt:

let columns = [GridItem(.adaptive(minimum: 220))]

ScrollView {
    LazyVGrid(columns: columns) {
        ForEach(boards) { board in
            BoardCard(board: board)
        }
        .reorderable()
    }
}
.reorderContainer(for: Board.self) { boards.apply($0) }

Apples Leitfaden merkt an, dass das Sortieren nur Verschiebungen innerhalb des Containers abdeckt. Elemente in andere Bereiche Ihrer App zu ziehen oder aus anderen Apps hinein, ist ein separater Schritt mit dragContainer(for:in:_:) und dropDestination(for:isEnabled:action:). Wenn das innere Display mit ArrangementView zwei Bereiche nebeneinander zeigt, behandelt unser Leitfaden zu ArrangementView die Aufteilung selbst.

Die wichtigsten Punkte

  • In iOS 27 ergeben .reorderable() an einem ForEach und .reorderContainer(for:) an seinem Container Drag-to-Reorder ohne Bearbeitungsmodus und ohne .onMove.
  • Der Container kann eine List, ein Stack, ein Grid oder ein eigenes Layout sein.
  • Die move-Closure erhält eine ReorderDifference: verschobene IDs in sources und ein destination mit .before(_:) oder .end. Wenden Sie sie selbst auf Ihr Array an.
  • Verwenden Sie reorderable(collectionID:) und reorderContainer(for:in:), um Elemente zwischen Sections zu verschieben.
  • Swipe Actions funktionieren jetzt außerhalb von List; fügen Sie .swipeActionsContainer() zur ScrollView oder zum Layout hinzu.
  • Alles hier gilt ab iOS 27.0, sichern Sie es also mit #available ab und behalten Sie .onMove als Fallback.

Listen sind nur die Hälfte einer ausgefeilten UI; auch der Text in den Zeilen zählt. Lesen Sie als Nächstes unseren Leitfaden zur Zeilenhöhe in SwiftUI unter iOS 26, und entdecken Sie unter Built for the fold Apps und Spiele, die das iPhone Duo bereits optimal nutzen.

Häufige Fragen

Wie sortiere ich eine SwiftUI List in iOS 27 ohne Bearbeitungsmodus?

Wenden Sie .reorderable() auf das ForEach in der List an und .reorderContainer(for:) auf die List selbst, mit Ihrem Elementtyp als Argument. Nutzer können Zeilen dann direkt ziehen, und die move-Closure liefert Ihnen eine ReorderDifference, die Sie auf Ihre Daten anwenden. EditButton und onMove werden nicht benötigt.

Funktioniert reorderable() in LazyVStack und LazyVGrid?

Ja. Laut Apples Dokumentation kann ein sortierbarer Container eine Liste, ein Stack, ein Grid oder ein eigenes Layout sein. Setzen Sie .reorderContainer auf diesen Container und .reorderable() auf das ForEach darin.

Was ist ReorderDifference in SwiftUI?

Das ist der Wert, den die move-Closure am Ende eines Drags erhält. Die Eigenschaft sources listet die IDs der verschobenen Elemente in der Reihenfolge, in der die Person sie ausgewählt hat, und destination gibt an, wohin sie wandern: vor ein bestimmtes Element oder ans Ende einer Collection.

Kann ich in iOS 27 Swipe Actions außerhalb einer List verwenden?

Ja. Fügen Sie jeder Zeile wie gewohnt .swipeActions hinzu und wenden Sie .swipeActionsContainer() auf die umgebende ScrollView oder das Layout an. Der Container sorgt dafür, dass immer nur eine Zeile geöffnet ist und dass Scrollen oder Tippen an anderer Stelle sie schließt.

Sind reorderable() und swipeActionsContainer() auch unter iOS 26 verfügbar?

Nein. Beide sind als verfügbar ab iOS 27.0 dokumentiert, daher brauchen Apps, die ältere Versionen unterstützen, eine if #available-Prüfung mit einem Fallback wie onMove in einer List.

Quellen

  1. reorderable() · Apple Developer Documentation
  2. reorderContainer(for:isEnabled:move:) · Apple Developer Documentation
  3. reorderContainer(for:in:isEnabled:move:) · Apple Developer Documentation
  4. Reordering items in lists, stacks, grids, and custom layouts · Apple Developer Documentation
  5. ReorderDifference · Apple Developer Documentation
  6. onMove(perform:) · Apple Developer Documentation
  7. swipeActionsContainer() · Apple Developer Documentation
  8. swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:) · Apple Developer Documentation
  9. Code-along: Build powerful drag and drop in SwiftUI (WWDC26) · Apple Developer
  10. swipeActionsContainer() does not mirror the swipe gesture in right-to-left layouts · Apple Developer Forums
  11. iOS 27: swipeActions outside List never re-evaluates its content closure · Apple Developer Forums
  12. iOS 27 gives ForEach its own drag to reorder: .reorderable() and .reorderContainer · @sarunw on X
  13. No .onMove. No edit mode. · @sarunw on X
  14. Swipe actions used to need a List row; in iOS 27 any view can have them · @sarunw on X
  15. New SwiftUI APIs for reordering and drag and drop on iOS 27 · Nil Coalescing
  16. Custom scroll layouts with swipe actions in SwiftUI on iOS 27 · Nil Coalescing
  17. Swipe actions outside of List in SwiftUI · Swift with Majid
  18. How to let users move rows in a list · Hacking with Swift

Machen Sie das Scharnier zum Teil Ihrer App.

Wir stellen Apps und Spiele vor, die mit dem iPhone Duo wirklich etwas anfangen. Schicken Sie uns Ihre mit einem Link zum App Store – oder sehen Sie sich an, was andere gebaut haben.

[email protected]