With iOS 27, SwiftUI drops two old restrictions at once: swipe actions are no longer tied to List, and drag-to-reorder expands beyond List into LazyVGrid, LazyVStack, and custom layouts. WWDC26's SwiftUI guide announces both capabilities side by side, deliberately, in the same "Presentation and interaction" section. This post covers swipeActionsContainer(), .reorderable(), and .reorderContainer(for:) with real code examples, when each works in which container, and how to keep your data model in sync with reordering.
💡 Pro Tip:swipeActionsContainer()andreorderContainer(for:)are two separate modifiers — don't assume applying one brings the other along automatically; you need to add both to their own containers separately.
Table of Contents
- The old way: why List was required
- Swipe in a ScrollView with swipeActionsContainer()
- Reordering in grids and stacks: the List boundary lifts
- .reorderable() and .reorderContainer(for:) API
- Keeping the data model in sync with ReorderDifference
- Reordering in sectioned lists
- Reordering comes to watchOS for the first time
- Platform scope: the tvOS exception
- Decision table: onMove or reorderable?
- FAQ
- How do I add a swipe action to a LazyVGrid?
- How do you use swipeActionsContainer?
- How do you drag-to-reorder outside of List in SwiftUI?
- When should you use reorderable instead of onMove?
- Full swipe and multiple actions
- Matching the type in reorderContainer(for:) correctly
- Binding reordering to edit mode with isEnabled
- Conclusion
- Sources
The old way: why List was required
Before iOS 27, the swipeActions(edge:allowsFullSwipe:content:) modifier only had an effect on List rows. In Apple's official API reference's platform-compatibility table, this modifier has existed since iOS 15, but its behavior was always tied to List's row mechanism — putting the same modifier inside any other ScrollView did nothing.
That's why developers building custom list views with ScrollView + LazyVStack had to hand-write swipe behavior with DragGesture and manual offset math — a repetitive, fragile piece of code in every app. onmyway133 confirms the same restriction: "Without swipeActionsContainer() on the container, the row level modifier still has no effect outside a List." So the restriction was real, and it lasted until iOS 27.
Reordering had the same problem: drag-to-reorder outside List meant third-party libraries or low-level DragGesture plus coordinate math. onMove(perform:) was tied only to List's EditMode flow — it compiled inside a LazyVGrid-style layout, but produced no effect there.
Swipe in a ScrollView with swipeActionsContainer()
iOS 27 lifts this restriction with the swipeActionsContainer() modifier. WWDC26's SwiftUI guide states it plainly: "Add swipeActionsContainer to any ScrollView to enable them across your layout." Apply it once, to the wrapping ScrollView, and the normal swipeActions() calls on every row inside become active.
In practice it looks like this:
1struct InboxView: View {2 @State private var messages: [Message] = Message.samples3 4 var body: some View {5 ScrollView {6 LazyVStack(spacing: 8) {7 ForEach(messages) { message in8 MessageRow(message: message)9 .swipeActions(edge: .trailing, allowsFullSwipe: true) {10 Button(role: .destructive) {11 delete(message)12 } label: {13 Label("Sil", systemImage: "trash")14 }15 Button {16 archive(message)17 } label: {18 Label("Arşivle", systemImage: "archivebox")19 }20 }21 }22 }23 }24 .swipeActionsContainer()25 }26}Note: .swipeActionsContainer() goes on the outermost ScrollView, while .swipeActions() goes on each row separately. Without the container modifier, these row-level swipeActions() calls silently do nothing outside List — and per swiftwithmajid, only one row's swipe menu stays open at a time; it closes automatically on scroll or a tap outside the container.
Reordering in grids and stacks: the List boundary lifts
The change is more fundamental on the reordering side. The WWDC26 guide says: "New reorderable container APIs let people drag to rearrange items in any container — not just List — using the same code across List, LazyVGrid, and more." So the exact same pair of modifiers works, unchanged, on List, LazyVGrid, LazyVStack, or a custom Layout protocol.
In practice: if you show a photo gallery with LazyVGrid, you no longer need to abandon that grid design and go back to List just so users can drag cells to reorder them. The same .reorderable() + .reorderContainer(for:) pair works on grid cells too.
1struct PhotoGridView: View {2 @State private var photos: [Photo] = Photo.samples3 let columns = [GridItem(.adaptive(minimum: 100))]4 5 var body: some View {6 ScrollView {7 LazyVGrid(columns: columns, spacing: 8) {8 ForEach(photos) { photo in9 PhotoThumbnail(photo: photo)10 }11 .reorderable()12 }13 .reorderContainer(for: Photo.self) { difference in14 move(difference: difference)15 }16 }17 }18}If you swapped LazyVGrid for List, these two modifier lines would stay exactly the same — that's the concrete payoff of Apple's "same code in every container" promise.
.reorderable() and .reorderContainer(for:) API
The division of labor between the two modifiers is clear: .reorderable() is added to the ForEach declaration to mark which views should be draggable. .reorderContainer(for:isEnabled:move:) is added to the enclosing List, stack, grid, or custom layout container to define where reordering is allowed to happen.
Apple's official article defines it this way: "indicate which views you want people to reorder by adding the reorderable() modifier to the ForEach declaration that generates those views" and "define the area in your interface where people can reorder views by adding the reorderContainer(for:isEnabled:move:) modifier to the enclosing list, stack, grid, or custom layout container."
Apple's article also sets a precondition: the data item's identifier must conform to Hashable and Sendable (build Identifiable conformance with such an identifier). The isEnabled parameter lets you toggle reordering at runtime — for example, bound to an "edit mode" switch:
1LazyVStack {2 ForEach(tasks) { task in3 TaskRow(task: task)4 }5 .reorderable()6}7.reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in8 move(difference: difference)9}When isEnabled: false, drag gestures are completely disabled, and you don't need to rebuild the view hierarchy.
Keeping the data model in sync with ReorderDifference
When the drag ends, SwiftUI passes a ReorderDifference value to reorderContainer's closure. This value describes the moved item(s) (sources) and the destination (destination); the destination can be .before(id) (before a specific item) or .end (the end of the list). Apple's own example wires this closure up like this:
1LazyVGrid(columns: columns) {2 ForEach(photos) { photo in3 PhotoThumbnail(photo: photo)4 }5 .reorderable()6}7.reorderContainer(for: Photo.self) { difference in8 move(difference: difference)9}The body of move(difference:) is yours to write — SwiftUI only gives you sources and destination in .before(id) / .end form; the rest (removing from the array, inserting at the target position) is standard Swift Array manipulation. SwiftUI itself never mutates the array; it only translates the gesture into these two parts (moved id + destination). If this closure is left empty, the drag stays a purely visual animation; the view keeps reflecting the data model's original order until you update the array.
Reordering in sectioned lists
If items are split into sections (e.g. "To Do" / "In Progress" / "Done" columns on a Kanban board), the plain .reorderable() + .reorderContainer(for:) pair isn't enough — SwiftUI offers a variant that also carries the section identity for this case: .reorderable(collectionID:) (on ForEach, to indicate which section an item belongs to) and reorderContainer(for:in:) (on the container, to enable moves across sections).
1ScrollView {2 LazyVStack {3 ForEach(sections) { section in4 Section(section.title) {5 ForEach(section.items) { item in6 TaskCard(item: item)7 }8 .reorderable(collectionID: section.id)9 }10 }11 }12 .reorderContainer(for: TaskItem.self, in: TaskSection.ID.self) { difference in13 move(difference: difference)14 }15}With this variant you can drag an item not just within its own section but from one section to another — the kind of need that comes up in real-world scenarios like Kanban boards and checklist groups.
Reordering comes to watchOS for the first time
The most striking line in iOS 27's reordering API concerns watchOS. The WWDC26 guide says it outright: "Reordering comes to watchOS for the first time." This shows that the .reorderable() / .reorderContainer(for:) pair was designed not just to "also work outside List," but to "bring reordering to a platform that never had it before."
In practice, this means users can drag to reorder a list or grid on a watchOS app even on the small screen — previously there was no official SwiftUI API for this. The same reorderContainer(for:) call also works inside a more compact, watchOS-specific stack instead of List; no extra platform check is needed, since the API is already the same.
It's the reordering that's "first"; swipe actions already existed on watchOS inside List.
Platform scope: the tvOS exception
Neither feature ships on every platform. Per onmyway133's summary, reordering works on "iOS, macOS, watchOS, and visionOS 27" and is "unavailable on tvOS" — the same source says swipeActionsContainer "remains unavailable on tvOS" as well. In short, tvOS is excluded from both new APIs — a design decision consistent with tvOS's remote-control-based interaction model (no touch/drag gestures).
Feature | iOS 27 | macOS 27 | watchOS 27 | visionOS 27 | tvOS 27 |
|---|---|---|---|---|---|
swipeActionsContainer() (swipe in any ScrollView) | Yes | Yes | Yes | Yes | No |
.reorderable() / .reorderContainer(for:) (reordering outside List) | Yes | Yes | Yes (first time) | Yes | No |
One thing to watch when reading the table: the "swipe" in the watchOS row already existed inside List before (since the iOS 15 / watchOS 8 generation); what's new is general reordering support arriving on watchOS, not swipe itself.
Decision table: onMove or reorderable?
onMove(perform:) is still in SwiftUI and hasn't gone away. But now you have two different drag-to-reorder paths, so it's worth knowing which one to pick:
Situation | Recommended API | Why |
|---|---|---|
Simple reordering inside a List, together with EditMode | onMove(perform:) | Already built into List's edit flow, needs no extra code |
Reordering inside LazyVGrid, LazyVStack, or a custom Layout | .reorderable() + .reorderContainer(for:) | onMove compiles in these containers but has no effect; it's List-only |
Reordering support needed on watchOS | .reorderable() + .reorderContainer(for:) | Per the WWDC26 guide, watchOS gets reordering for the first time via this API |
Cross-section (Kanban-style) moves | .reorderable(collectionID:) + reorderContainer(for:in:) | The only variant that carries the section identity |
In short: if you're staying inside List and don't want to change the EditMode flow, onMove is still the option that requires the least code. But the moment you step outside List, the new API is your only option — the old method simply had no counterpart in these containers.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
You can use the checklist below to apply the four APIs in this article (swipeActionsContainer, reorderable, reorderContainer, reorderContainer(for:in:)) in the right order. Don't move to the next step without checking off each one; the order matters, because without the container modifier, the row modifiers silently do nothing.
FAQ
How do I add a swipe action to a LazyVGrid?
LazyVGrid itself doesn't support swipeActions; the behavior outside List was added in WWDC26. You apply swipeActionsContainer() to the ScrollView wrapping the grid, then add the normal swipeActions(edge:allowsFullSwipe:content:) modifier to each grid cell. Without the container modifier, the cell-level swipeActions() has no effect at all — in onmyway133's words, "Without swipeActionsContainer() on the container, the row level modifier still has no effect outside a List."
How do you use swipeActionsContainer?
swipeActionsContainer() is applied to a ScrollView (or a parent view wrapping it) and activates the swipeActions() calls on all child views inside it. In the WWDC26 guide's own words: "Add swipeActionsContainer to any ScrollView to enable them across your layout." Only one row's swipe menu stays open at a time; it closes automatically when you scroll or tap outside the container.
How do you drag-to-reorder outside of List in SwiftUI?
You add .reorderable() to the ForEach, and .reorderContainer(for:) to the container wrapping it (LazyVStack, LazyVGrid, HStack/VStack, or a custom layout). When the drag ends, SwiftUI produces a ReorderDifference; this value describes the moved item and the destination position (.before(id) or .end), and you use that information in the move(difference:) function to actually update the array. For sectioned content there's the reorderable(collectionID:) + reorderContainer(for:in:) variant. The WWDC26 guide says this works with the same code across "List, LazyVGrid, and more."
When should you use reorderable instead of onMove?
onMove only works inside List, in the context of EditMode. reorderable()/reorderContainer(for:) is preferred when you need drag-to-reorder outside List (grid, stack, custom layout), and when you need reordering support on watchOS — per the WWDC26 guide, reordering comes to watchOS for the first time through this API. If you're staying inside List and simple moves are enough without changing the EditMode flow, onMove is still a valid option that requires less code.
Full swipe and multiple actions
swipeActionsContainer() doesn't just move the "open/close" mechanic outside List, it brings allowsFullSwipe along too. With allowsFullSwipe: true, swiping a row all the way fires the first action (usually .destructive) automatically — the same behavior you know from List, now also valid on ScrollView + LazyVStack:
1MessageRow(message: message)2 .swipeActions(edge: .trailing, allowsFullSwipe: true) {3 Button(role: .destructive) {4 delete(message)5 } label: {6 Label("Sil", systemImage: "trash")7 }8 }9 .swipeActions(edge: .leading) {10 Button {11 markAsRead(message)12 } label: {13 Label("Okundu İşaretle", systemImage: "envelope.open")14 }15 .tint(.blue)16 }One thing to note: you need separate swipeActions calls for edge: .leading and edge: .trailing — you can't define both edges in a single call. Even with multiple buttons, a full swipe triggers the first action; if you want to disable this behavior for one edge, pass allowsFullSwipe: false.
Matching the type in reorderContainer(for:) correctly
The for: parameter in the reorderContainer(for:) call must exactly match the element type of the array ForEach operates on — in Apple's example this is Photo.self, because ForEach(photos) operates on a [Photo] array:
1LazyVGrid(columns: columns) {2 ForEach(photos) { photo in3 PhotoThumbnail(photo: photo)4 }5 .reorderable()6}7.reorderContainer(for: Photo.self) { difference in8 move(difference: difference)9}This is a natural consequence of Swift's generic type inference: reorderContainer(for:) figures out which collection it operates on from this parameter. If your own project uses more than one reorderContainer (say, both a photo list and a tag list are reorderable), make sure each reorderContainer(for:) call matches the element type of its own ForEach.
In sectioned reordering, this matching gets one step more complex: reorderContainer(for:in:) takes both the item type and the section identifier (collectionID) TYPE (e.g. TaskSection.ID.self), and this must also be consistent with the section identifier type you pass to ForEach via reorderable(collectionID:).
Binding reordering to edit mode with isEnabled
The isEnabled parameter in the reorderContainer(for:isEnabled:move:) signature lets you tie reordering to a specific mode instead of keeping it always on. A typical use is a state that's toggled by an "Edit" button at the top of the screen:
1struct TaskListView: View {2 @State private var tasks: [TaskItem] = TaskItem.samples3 @State private var isEditing = false4 5 var body: some View {6 VStack {7 Toggle("Düzenle", isOn: $isEditing)8 .toggleStyle(.button)9 .padding(.horizontal)10 11 ScrollView {12 LazyVStack {13 ForEach(tasks) { task in14 TaskRow(task: task)15 }16 .reorderable()17 }18 .reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in19 move(difference: difference)20 }21 }22 }23 }24}When isEnabled: false, drag gestures on reorderable() views are disabled, without rebuilding the view hierarchy — flipping a single @State variable is enough. A practical safeguard against accidentally shuffling a list (say, in a read-only view).
There's no equivalent isEnabled parameter for swipeActionsContainer(); to conditionally disable swipe actions, you wrap the swipeActions() call in an if block.
Conclusion
iOS 27 lifts two separate but complementary restrictions in SwiftUI at once: swipe actions now work in any ScrollView via swipeActionsContainer(), and drag-to-reorder expands beyond List into LazyVGrid, LazyVStack, and watchOS via .reorderable() + .reorderContainer(for:). Both are independent systems; using one doesn't automatically bring the other, but they can also be used together on the same screen.
Using these APIs doesn't change how you manage your data model with @State — see our article on where the @State macro breaks with ContentBuilder in iOS 27. Syncing data after reordering with SwiftData? Check out SwiftData's enum predicate and sectioned query support. Want Siri/App Intents control too? Read about App Intents' support for long-running intents.
If you're building your own component library on top of custom layouts, the SwiftUI custom component library guide and the custom Layout protocol guide will help you understand how reorderContainer in this article combines with custom layouts.
Sources
- WWDC26 SwiftUI Guide — Apple's official WWDC26 SwiftUI overview page; the direct-quote source for the swipeActionsContainer and reorderable/reorderContainer announcements.
- [Apple Developer — swipeActions(edge:allowsFullSwipe:content:)](<https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)>) — the official API reference, the modifier's original List-dependent definition.
- Apple Developer — Reordering items in lists, stacks, grids, and custom layouts — Apple's official article and code examples for reorderable() and reorderContainer(for:).
- onmyway133 — What's new in SwiftUI in iOS 27 — the source for platform scope (watchOS first time, no tvOS) and ReorderDifference details.
- Swift with Majid — Swipe actions outside of List in SwiftUI — details on swipeActionsContainer's one-row-at-a-time behavior and scroll/tap-outside dismissal.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

