iOS 27 adds powerful new drag and drop APIs to SwiftUI: a reorderable modifier for rearranging ForEach content, a dragContainer for lifting multiple items simultaneously, and dragConfiguration/dropConfiguration for controlling how data is transferred (copy vs. move).
⢠The new reorderable + reorderContainer modifiers replace boilerplate onMove/onInsert code, enabling cross-list reordering with a single modifier pair
⢠dragContainer lets users lift multiple items at once ā previously impossible without UIKit interop
⢠dragConfiguration and dropConfiguration give fine-grained control over copy vs. move semantics and validation, enabling game-like and document-centric UX patterns
Demonstrates the new reorderable and reorderContainer modifiers to enable cross-pile drag-and-drop reordering of cards, plus dragPreviewsFormation for a stacked visual effect.
import SwiftUI
struct CardValue: Transferable, Identifiable, Hashable {
let id: UUID
let rank: String
let suit: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .data)
}
}
extension CardValue: Codable {}
enum PileID: String, Hashable {
case left, right
}
struct MultiPileReorderView: View {
@State private var leftPile: [CardValue] = [
CardValue(id: UUID(), rank: "A", suit: "ā "),
CardValue(id: UUID(), rank: "2", suit: "ā "),
CardValue(id: UUID(), rank: "3", suit: "ā ")
]
@State private var rightPile: [CardValue] = [
CardValue(id: UUID(), rank: "4", suit: "ā„"),
CardValue(id: UUID(), rank: "5", suit: "ā„"),
CardValue(id: UUID(), rank: "6", suit: "ā„")
]
var body: some View {
HStack(spacing: 32) {
pileView(title: "Left", cards: $leftPile, pileID: .left)
pileView(title: "Right", cards: $rightPile, pileID: .right)
}
.padding()
.reorderContainer(itemType: CardValue.self, id: PileID.self) { difference, sourceID, destinationID in
applyDifference(difference, sourceID: sourceID, destinationID: destinationID)
}
.dropPreviewsFormation(.stack)
}
@ViewBuilder
func pileView(title: String, cards: Binding<[CardValue]>, pileID: PileID) -> some View {
VStack(spacing: 8) {
Text(title).font(.headline)
ForEach(cards) { card in
Text("\(card.rank)\(card.suit)")
.frame(width: 60, height: 80)
.background(RoundedRectangle(cornerRadius: 8).fill(.white).shadow(radius: 2))
.reorderable()
}
}
.reorderableContainerMember(id: pileID)
}
func applyDifference(
_ difference: CollectionDifference<CardValue>,
sourceID: PileID?,
destinationID: PileID?
) {
// Apply removals from source pile
if let sourceID {
var source = pile(for: sourceID)
for removal in difference.removals {
if case .remove(let offset, _, _) = removal {
source.remove(at: offset)
}
}
setPile(source, for: sourceID)
}
// Apply insertions to destination pile
if let destinationID {
var destination = pile(for: destinationID)
for insertion in difference.insertions {
if case .insert(let offset, let element, _) = insertion {
destination.insert(element, at: offset)
}
}
setPile(destination, for: destinationID)
}
}
func pile(for id: PileID) -> [CardValue] {
id == .left ? leftPile : rightPile
}
func setPile(_ cards: [CardValue], for id: PileID) {
if id == .left { leftPile = cards } else { rightPile = cards }
}
}
#Preview {
MultiPileReorderView()
.background(Color.green.opacity(0.4))
}reorderContainer must use the same Transferable item type as the reorderable modifiers it contains; dragContainer and reorderContainer must share the same item type to interoperate; dropConfiguration has final say over copy vs. move regardless of dragConfiguration intent; multiple reorderable modifiers in the same container each require a unique ID
No special hardware required; drag and drop requires a device or simulator that supports the gesture (iPadOS/macOS for full multi-item experience)
More iOS 27 APIs land every week.
Get notified when new capabilities are published ā no noise, just signal.