Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2026 App ServicesSwiftUI & UI Frameworks

WWDC26 · 15 min · App Services / SwiftUI & UI Frameworks

Code-along: Build powerful drag and drop in SwiftUI

Follow along as we build a game of Solitaire to explore the latest drag-and-drop capabilities in SwiftUI. We’ll show you how to use the new reordering API to let people arrange content, implement drag containers to move multiple items at once, and customize the drag-and-drop lifecycle to fit your app’s rules. To get the most out of this session, watch “Meet Transferable” from WWDC22.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 8 snippets

Add reorderable to the preview swift · at 3:40 ↗
#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()
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .reorderContainer(for: CardValue.self) { difference in
        cards.apply(difference: difference)
    }
    .padding()
    .background(.green.gradient)
}
Add reorder container to the GameView swift · at 4:40 ↗
struct GameView: View {
    var game: Game

    var body: some View {
        GeometryReader { proxy in
            let spacing: CGFloat = 10
            let cardWidth = (proxy.size.width - 6 * spacing) / 7
            VStack {
                HStack(alignment: .top, spacing: spacing) {
                    Group {
                        RemainderView(game: game)
                        CardBackView()
                            .hidden()
                        ForEach(CardValue.Suit.allCases) { suit in
                            DestinationView(game: game, suit: suit)
                        }
                    }
                    .frame(width: cardWidth)
                }
                .padding(.bottom, 20)
                HStack(alignment: .top, spacing: spacing) {
                    ForEach(0..<7) { index in
                        PileView(game: game, index: index)
                            .frame(width: cardWidth)
                    }
                }
                .frame(maxHeight: .infinity, alignment: .top)
              	// Add the reorder container modifier.
                .reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
                    game.moveCards(difference: difference)
                }
            }
        }
        .padding()
    }
}
Add reorderable to PileView swift · at 5:58 ↗
struct PileView: View {
    var game: Game
    var index: Int
    @Query var cards: [Card]

    var body: some View {
        ZStack(alignment: .topLeading) {
            CardPlaceholderView()
            PileLayout {
                let index = firstFaceUpIndex
              	// Iterates over the face down cards.
                ForEach(cards[..<index]) { card in
                    CardView(card: card)
                }
                // Iterates over the face up cards.
                ForEach(cards[index...], id: \.value) { card in
                    CardView(card: card)
                }
                .reorderable(collectionID: Card.Group.pile(index))
            }
        }
    }

    var firstFaceUpIndex: Int {
        cards.firstIndex { !$0.isFaceDown } ?? cards.endIndex
    }
}
Add dragContainer to customize the reorderContainer modifier. swift · at 7:50 ↗
struct GameView: View {
    var game: Game

    var body: some View {
        GeometryReader { proxy in
            let spacing: CGFloat = 10
            let cardWidth = (proxy.size.width - 6 * spacing) / 7
            VStack {
                HStack(alignment: .top, spacing: spacing) {
                    Group {
                        RemainderView(game: game)
                        CardBackView()
                            .hidden()
                        ForEach(CardValue.Suit.allCases) { suit in
                            DestinationView(game: game, suit: suit)
                        }
                    }
                    .frame(width: cardWidth)
                }
                .padding(.bottom, 20)
                HStack(alignment: .top, spacing: spacing) {
                    ForEach(0..<7) { index in
                        PileView(game: game, index: index)
                            .frame(width: cardWidth)
                    }
                }
                .frame(maxHeight: .infinity, alignment: .top)
                .reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
                    game.moveCards(difference: difference)
                }
                // Add dragContainer to customize reorderContainer.
                .dragContainer(for: CardValue.self) { cardID in
                    game.cardStack(startingAt: cardID)
                }
            }
        }
        .padding()
    }
}
Add dragPreviewsFormation to customize how the dragged cards appear swift · at 8:45 ↗
struct GameView: View {
    var game: Game

    var body: some View {
        GeometryReader { proxy in
            let spacing: CGFloat = 10
            let cardWidth = (proxy.size.width - 6 * spacing) / 7
            VStack {
                HStack(alignment: .top, spacing: spacing) {
                    Group {
                        RemainderView(game: game)
                        CardBackView()
                            .hidden()
                        ForEach(CardValue.Suit.allCases) { suit in
                            DestinationView(game: game, suit: suit)
                        }
                    }
                    .frame(width: cardWidth)
                }
                .padding(.bottom, 20)
                HStack(alignment: .top, spacing: spacing) {
                    ForEach(0..<7) { index in
                        PileView(game: game, index: index)
                            .frame(width: cardWidth)
                    }
                }
                .frame(maxHeight: .infinity, alignment: .top)
                .reorderContainer(for: CardValue.self, in: Card.Group.self) { difference in
                    game.moveCards(difference: difference)
                }
                .dragContainer(for: CardValue.self) { cardID in
                    game.cardStack(startingAt: cardID)
                }
              	// Have dragged cards appear as a stack.
                .dragPreviewsFormation(.stack)
            }
        }
        .padding()
    }
}
Add dropPreviewsFormation to customize how dragged cards appear over a destination swift · at 9:14 ↗
struct GameView: View {
    var game: Game

    var body: some View {
        GeometryReader { proxy in
            let spacing: CGFloat = 10
            let cardWidth = (proxy.size.width - 6 * spacing) / 7
            VStack {
                HStack(alignment: .top, spacing: spacing) {
                    Group {
                        RemainderView(game: game)
                        CardBackView()
                            .hidden()
                        ForEach(CardValue.Suit.allCases) { suit in
                            DestinationView(game: game, suit: suit)
                        }
                    }
                    .frame(width: cardWidth)
                }
                .padding(.bottom, 20)
                HStack(alignment: .top, spacing: spacing) {
                    ForEach(0..<7) { index in
                        PileView(game: game, index: index)
                            .frame(width: cardWidth)
                    }
                }
                .frame(maxHeight: .infinity, alignment: .top)
                .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)
            }
            // Have a consistent appearance over drop destinations.
            .dropPreviewsFormation(.stack)
        }
        .padding()
    }
}
Add a drag configuration to allow move. swift · at 11:40 ↗
struct RemainderView: View {
    @Query var cards: [Card]
    var game: Game

    var body: some View {
        Button {
            incrementCardIndex()
        } label: {
            ZStack {
                CardPlaceholderView()
                CardBackView()
                    .opacity(cards.isEmpty ? 0 : 1)
            }
        }
        .buttonStyle(.plain)
        .disabled(cards.isEmpty)
        ZStack {
            CardPlaceholderView()
            if let currentCard {
                CardFaceView(card: currentCard.value)
                    .draggable(containerItemID: currentCard.value)
                    .opacity(currentCard.value == hiddenCard ? 0 : 1)
            }
        }
        .dragContainer(for: CardValue.self) { cardID in
            [cardID]
        }
        // Add the drag configuration to allow me.
        .dragConfiguration(DragConfiguration(allowMove: true))
    }
}
Add a drop destination modifier and configure it swift · at 12:05 ↗
struct GameView: View {
    var game: Game

    var body: some View {
        GeometryReader { proxy in
            let spacing: CGFloat = 10
            let cardWidth = (proxy.size.width - 6 * spacing) / 7
            VStack {
                HStack(alignment: .top, spacing: spacing) {
                    Group {
                        RemainderView(game: game)
                        CardBackView()
                            .hidden()
                        ForEach(CardValue.Suit.allCases) { suit in
                            DestinationView(game: game, suit: suit)
                        }
                    }
                    .frame(width: cardWidth)
                }
                .padding(.bottom, 20)
                HStack(alignment: .top, spacing: spacing) {
                    ForEach(0..<7) { index in
                        PileView(game: game, index: index)
                            .frame(width: cardWidth)
                    }
                }
                .frame(maxHeight: .infinity, alignment: .top)
                .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)
                .dragConfiguration(DragConfiguration(allowMove: true))
                // Add a drop destination to accept inserts
                .dropDestination(for: CardValue.self) { newCards, session in
                    if let destination = session.reorderDestination(
                        for: CardValue.self, in: Card.Group.self) {
                        game.insertCards(newCards, to: destination)
                    }
                }
                // Configure where cards will go when reordering,
                // and accept them by move.
                .dropConfiguration { session in
                    // Calculate which pile is being dragged over.
                    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))
                    // Check if the move is allowed.
                    let allowed = session.suggestedOperations.contains(.move)
                    && game.validateMove(session: session, destination: destination)
                    let operation: DropOperation = allowed ? .move : .forbidden
                    return DropConfiguration(operation: operation, destination: destination)
                }
            }
            .dropPreviewsFormation(.stack)
        }
        .padding()
    }
}

Resources