Code-along: Build powerful drag and drop in SwiftUI
Build SwiftUI drag-and-drop interactions with reorderable content, multi-item drag containers, preview formations, and move-aware drop configuration.
TL;DR
- SwiftUI adds a reordering API built around
reorderableandreorderContainer, letting views move items within and across collections by applying aReorderDifference. dragContainercan customize what data is lifted for a drag, enabling multi-item drags such as moving a stack of Solitaire cards from a single gesture.dragPreviewsFormationanddropPreviewsFormationcontrol how multiple dragged items are visually grouped, with options such as stack-like previews.dragConfiguration,dropDestination, anddropConfigurationlet sources and destinations negotiate move/copy behavior and reject invalid drops based on app rules.
Context: SwiftUI drag and drop expands beyond Transferable
The session builds a Solitaire game to demonstrate new SwiftUI drag-and-drop capabilities available in the 2027 releases. It assumes the existing model where app data conforms to Transferable and views use draggable and dropDestination to move content through the system.
The new pieces focus on app-local interactions: reordering ordered content, dragging multiple items from a single gesture, controlling drag/drop preview formation, and configuring whether a transfer should be a move or a copy.
- Use
Transferablefor data that participates in drag and drop. - Use the new reordering APIs when content has an order that people should be able to rearrange.
- Compose lower-level drag/drop modifiers with
reorderContainerwhen the default behavior needs app-specific rules.
Enable reordering with reorderable and reorderContainer
The basic reordering model has two parts: mark the items that can be reordered with reorderable, then scope the interaction with reorderContainer. At the end of an operation, SwiftUI provides a difference that the app applies to its model.
For a single collection preview, the closure can directly apply the difference to an array. For the Solitaire board, one reorderContainer wraps all piles and uses a collection identifier type, Card.Group, so cards can move across multiple piles.
- Apply
reorderable()to theForEachor item collection that should participate in reordering. - Apply
reorderContainer(for:)around the shared container and update model state from the provided difference. - When multiple reorderable collections share a container, pass a unique
collectionIDfor each collection. - Exclude non-draggable content, such as face-down cards, by rendering it in a separate
ForEachwithoutreorderable.
Simple reorderable preview
Marks the card views reorderable and applies the resulting reorder difference to the backing array.
#Preview {
@Previewable @State var cards = [
CardValue(rank: .ace, suit: .clubs),
CardValue(rank: .ace, suit: .diamonds),
CardValue(rank: .ace, suit: .hearts),
CardValue(rank: .ace, suit: .spades)
]
HStack {
ForEach(cards) { card in
CardFaceView(card: card)
}
.reorderable()
}
.reorderContainer(for: CardValue.self) { difference in
cards.apply(difference: difference)
}
}Multiple piles in one reorder container
Scopes reordering across all Solitaire piles and lets game logic handle moves between collections.
HStack(alignment: .top, spacing: spacing) {
ForEach(0..<7) { index in
PileView(game: game, index: index)
.frame(width: cardWidth)
}
}
.reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
game.moveCards(difference: difference)
}Model reorderable subsets correctly
The session avoids advanced filtering by changing the view structure: face-down cards are rendered separately from face-up cards. Only the face-up slice receives reorderable, so dragging a face-down card starts no reordering interaction.
- Use view composition to express what is reorderable instead of accepting every item and later rejecting some of them.
- Use stable item identity for reorderable views; the sample uses
id: \.valuefor the face-up cards. - Provide the collection identifier with
reorderable(collectionID:)when a container manages multiple collections.
Only face-up cards are reorderable
Splits the pile into non-reorderable face-down cards and reorderable face-up cards.
PileLayout {
let index = firstFaceUpIndex
ForEach(cards[..<index]) { card in
CardView(card: card)
}
ForEach(cards[index...], id: \.value) { card in
CardView(card: card)
}
.reorderable(collectionID: Card.Group.pile(index))
}Drag multiple items with dragContainer and preview formations
A reorderContainer implicitly provides drag-container and drop-destination behavior, but the app can add its own dragContainer to customize what transferable data is included in a drag. In the Solitaire sample, dragging a card can lift that card plus the cards stacked above it.
The visual grouping of dragged items is configurable. dragPreviewsFormation(.stack) changes the lifted preview, and dropPreviewsFormation(.stack) keeps the same appearance when the drag is over destinations.
- Place
dragContainer(for:)near thereorderContainerwhen customizing reorder drags. - Use the same item type for the
reorderContaineranddragContainerso they work together. - Return all transferable items that should participate in the drag from the
dragContainerclosure. - Use
dragPreviewsFormationanddropPreviewsFormationto make multi-item drags visually match the app's interaction model.
Lift a stack of cards from one dragged card
Customizes the reorder drag so one card identifier expands into a stack of cards, then displays those previews as a stack.
.reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
game.moveCards(difference: difference)
}
.dragContainer(for: CardValue.self) { cardID in
game.cardStack(startingAt: cardID)
}
.dragPreviewsFormation(.stack)Keep drop previews consistent
Applies the same stacked preview formation over drop destinations.
VStack {
// playing area
}
.dropPreviewsFormation(.stack)Configure move semantics and validate drops
The remaining Solitaire interaction moves a card from the remainder deck into a pile. By default, SwiftUI suggests copy semantics for a separate drag source and drop destination. The source uses dragConfiguration(DragConfiguration(allowMove: true)) to express that a move is allowed, but the destination has the final say.
The destination adds dropDestination so the reorder container can accept inserted cards from outside the container, then adds dropConfiguration to compute the intended reorder destination, require .move, and reject invalid Solitaire moves with .forbidden.
- Use
dragConfigurationon the source to express transfer intent, such as allowing move. - Use
dropDestinationwhen a reorder container should accept new items, not only moves already inside the container. - Use
session.reorderDestination(for:in:)to read the destination computed for inserted reorder items. - Use
dropConfigurationto choose theDropOperation, provide a destination, and enforce app-specific validation.
Allow moving a card from the remainder deck
Configures the source card so a drag can represent a move instead of only a copy.
CardFaceView(card: currentCard.value)
.draggable(containerItemID: currentCard.value)
.dragContainer(for: CardValue.self) { cardID in
[cardID]
}
.dragConfiguration(DragConfiguration(allowMove: true))Accept and validate inserted cards
Lets the destination insert outside cards only when the session supports move and the game rules allow the computed pile destination.
.dropDestination(for: CardValue.self) { newCards, session in
if let destination = session.reorderDestination(
for: CardValue.self,
in: Card.Group.self
) {
game.insertCards(newCards, to: destination)
}
}
.dropConfiguration { session in
let alignedX = session.location.x - 0.5 * spacing
let pile = Int(alignedX / (cardWidth + spacing))
let destination = ReorderDifference<CardValue, Card.Group>.Destination(
position: .end,
collectionID: .pile(pile)
)
let allowed = session.suggestedOperations.contains(.move)
&& game.validateMove(session: session, destination: destination)
let operation: DropOperation = allowed ? .move : .forbidden
return DropConfiguration(operation: operation, destination: destination)
}Resources
Related Sessions
What’s new in SwiftUI
SwiftUI 2026 updates for Liquid Glass, document apps, reorderable containers, toolbar control, AsyncImage caching, @State, and ContentBuilder.
Use SwiftUI with AppKit and UIKit
Incrementally adopt SwiftUI in AppKit or UIKit apps using Observation, hosting views and menus, gesture recognizer representables, and SwiftUI scenes.