Reorder Items Across SwiftUI Sections Starting in Xcode 27

  • Jun 14

Reorder Items Across SwiftUI Sections Starting in Xcode 27

At WWDC 2026, Apple continued expanding SwiftUI's drag-and-drop capabilities with new APIs that make collection reordering significantly easier to implement.

Starting in Xcode 27, SwiftUI introduces the reorderable() and reorderContainer() APIs, allowing developers to add drag-and-drop reordering behavior directly to their views while maintaining full control over how data is updated.

Prior to this release, implementing cross-section drag-and-drop often required custom gesture handling, UIKit interoperability, or complex state management. The new APIs simplify this process by providing built-in support for reordering while still letting developers decide how items should be moved within their data source.

To demonstrate this functionality, we will build a course browser that displays courses grouped into sections. Users will be able to drag courses between sections and reorder them directly inside the grid.



Creating the Data Model

We will start with a simple data model representing a course.

struct DTCourses: Identifiable {
    var name: String
    var lessons: Int
    
    var id: String {
        return name + lessons.description
    }
}

This model stores the course name and lesson count. Conforming to Identifiable allows SwiftUI to uniquely track each course during updates and reordering operations.

Since our interface contains multiple sections, we also need a model that groups courses together.

struct DTCourseSection: Identifiable {
    var title: String
    var courses: [DTCourses]
    
    var id: String { title }
}

Each section contains a title and an array of courses. The section title acts as a unique identifier.

For sample data, we can create two categories.

extension DTCourses {
    static var claudeSample = [
        DTCourses(name: "Accelerated iOS Development with Claude Code", lessons: 19),
        DTCourses(name: "Agentic iOS App Development with Claude", lessons: 14),
    ]
    
    static var iOSSample = [
        DTCourses(name: "Ace Your iOS Developer Interview", lessons: 11),
        DTCourses(name: "Master In App Purchase in SwiftUI & iOS 26", lessons: 17),
    ]
}

These collections will populate our sections and provide content that can be reordered.

Building the Course Card

Each item is displayed using a custom card view.

struct CourseCardView: View {
    let course: DTCourses
    
    var body: some View {
        HStack(spacing: 16) {
            VStack(alignment: .leading, spacing: 6) {
                Text(course.name)
                    .font(.headline)
                
                HStack(spacing: 4) {
                    Image(systemName: "play.circle.fill")
                    
                    Text("\(course.lessons) lessons")
                }
            }
        }
        .padding()
        .background(
            .orange.opacity(0.3),
            in: .rect(cornerRadius: 10)
        )
    }
}

This view provides a reusable representation of a course and keeps the main implementation focused on layout and reordering logic.

Creating the Initial Grid

Our interface is built using a LazyVGrid containing multiple sections.

LazyVGrid(
    columns: Array(repeating: GridItem(), count: 3),
    pinnedViews: [.sectionHeaders]
) {
    ForEach(sections) { section in
        Section {
            ForEach(section.courses) { course in
                CourseCardView(course: course)
            }
        } header: {
            Text(section.title)
        }
    }
}

At this point the grid is completely static.

Users can browse courses and view section headers, but they cannot move items between sections.

The next step is enabling drag-and-drop reordering.

Making Items Reorderable

Enabling this behavior is surprisingly simple.

Apply the new reorderable() modifier to the collection of items.

ForEach(section.courses) { course in
    CourseCardView(course: course)
}
.reorderable(collectionID: section.id)

The collectionID identifies which collection currently owns the items.

SwiftUI uses this information to determine where an item originates and where it is being dropped during a drag operation.

After adding this modifier, SwiftUI automatically enables drag interactions for the items.

However, SwiftUI still needs to know how the underlying data should be updated when a move occurs.

Introducing reorderContainer()

This is where reorderContainer() becomes important.

.reorderContainer(
    for: DTCourses.self,
    in: String.self
) { difference in
    
}

The closure receives a difference object describing the move operation.

This object contains:

  • The items being moved

  • The source collection

  • The destination collection

  • The insertion position

Unlike older drag-and-drop approaches, SwiftUI does not directly modify your data source.

Instead, it provides all the information necessary for you to update your model yourself.

This gives you complete control over how reordering is handled.

Identifying the Moved Items

The first step is determining which items are being moved.

var moved: [DTCourses] = []

for id in difference.sources {
    for sectionIndex in sections.indices {
        if let courseIndex = sections[sectionIndex]
            .courses
            .firstIndex(where: { $0.id == id }) {
            
            moved.append(
                sections[sectionIndex].courses[courseIndex]
            )
            
            sections[sectionIndex]
                .courses
                .remove(at: courseIndex)
            
            break
        }
    }
}

Here we iterate through every source identifier involved in the drag operation.

Once we find the matching course, we store it temporarily and remove it from its original location.

At this stage the item has been detached from its source section but has not yet been inserted into its destination.

Locating the Destination Section

Next, we determine where the item is being dropped.

let targetSectionID =
    difference.destination.collectionID

guard let targetSectionIndex =
    sections.firstIndex(
        where: { $0.id == targetSectionID }
    )
else {
    return
}

The destination contains the target collection identifier.

Using that identifier, we locate the appropriate section inside our data source.

Once we have the section index, we can insert the moved items.

Handling Insert Positions

SwiftUI provides two possible insertion positions.

An item can be inserted before another item or appended to the end of the collection.

switch difference.destination.position {
case .before(let targetID):

case .end:

}

Let's handle each case separately.

Inserting Before an Existing Item

case .before(let targetID):
    if let targetIndex =
        sections[targetSectionIndex]
        .courses
        .firstIndex(where: { $0.id == targetID }) {
        
        sections[targetSectionIndex]
            .courses
            .insert(
                contentsOf: moved,
                at: targetIndex
            )
    }

When SwiftUI provides a target identifier, we locate that item and insert the moved courses directly before it.

This preserves the exact ordering requested by the user.

Appending to the End

case .end:
    sections[targetSectionIndex]
        .courses
        .append(contentsOf: moved)

If the destination position is .end, we simply append the moved items to the destination section.

This occurs when a user drops an item into empty space at the end of a collection.

Putting Everything Together

After combining the reordering APIs with the move logic, SwiftUI provides a fully interactive drag-and-drop experience across sections.

The core implementation consists of:

.reorderable(collectionID: section.id)

.reorderContainer(
    for: DTCourses.self,
    in: String.self
) { difference in
    
    var moved: [DTCourses] = []
    
    for id in difference.sources {
        for sectionIndex in sections.indices {
            if let courseIndex = sections[sectionIndex]
                .courses
                .firstIndex(where: { $0.id == id }) {
                
                moved.append(
                    sections[sectionIndex]
                        .courses[courseIndex]
                )
                
                sections[sectionIndex]
                    .courses
                    .remove(at: courseIndex)
                
                break
            }
        }
    }
    
    let targetSectionID =
        difference.destination.collectionID
    
    guard let targetSectionIndex =
        sections.firstIndex(
            where: { $0.id == targetSectionID }
        )
    else {
        return
    }
    
    switch difference.destination.position {
    case .before(let targetID):
        if let targetIndex =
            sections[targetSectionIndex]
            .courses
            .firstIndex(where: { $0.id == targetID }) {
            
            sections[targetSectionIndex]
                .courses
                .insert(
                    contentsOf: moved,
                    at: targetIndex
                )
        }
        
    case .end:
        sections[targetSectionIndex]
            .courses
            .append(contentsOf: moved)
    }
}

Additional Features

In our example, each course also supports swipe actions.

.swipeActions {
    Button(role: .destructive) {
        for sectionIndex in sections.indices {
            sections[sectionIndex]
                .courses
                .removeAll { $0.id == course.id }
        }
    } label: {
        Label("Delete", systemImage: "trash")
    }
}

Because the grid supports both swipe gestures and drag interactions, we also apply:

.swipeActionsContainer()

This ensures SwiftUI properly coordinates gesture behavior between swipe actions and reordering operations.

The result is an interface that supports deleting courses, moving them within a section, and transferring them across sections with very little code.

Build and run

Summary

The new reorderable() and reorderContainer() APIs make drag-and-drop collection management significantly easier in SwiftUI.

Rather than building custom drag-and-drop systems, developers can now enable reordering with a few modifiers while maintaining full control over how data is updated.

Because the APIs expose source and destination information directly, they work well for grids, lists, grouped collections, Kanban-style interfaces, media browsers, course catalogs, and any application that requires moving items between sections.

With just a few modifiers and a small amount of update logic, you can now build sophisticated cross-collection reordering experiences entirely in SwiftUI.

Thank you for reading. If you found this article helpful and would like to support our work, visit DevTechie.com for in-depth SwiftUI, iOS, and Apple development courses designed to help you build real-world applications and stay current with the latest Apple technologies.

Happy coding, and I'll see you in the next article.