-
Code Along:SwiftUIによるパワフルなドラッグ&ドロップ機能の構築
一緒にソリティアゲームを作りながら、SwiftUIの最新のドラッグ&ドロップ機能について学びましょう。ユーザーがコンテンツを整理できる機能を並べ替えのための新しいAPIにより実装する方法、複数のアイテムを一度に移動できるドラッグコンテナを実装する方法、アプリのルールに合わせてドラッグ&ドロップのライフサイクルをカスタマイズする方法を紹介します。このセッションの内容を十分理解できるよう、WWDC22の「Meet Transferable」を視聴することをおすすめします。
関連する章
- 0:00 - Introduction
- 1:42 - Reordering
- 6:50 - Drag multiple items
- 9:59 - Drag configuration
- 14:29 - Next steps
リソース
関連ビデオ
WWDC22
-
このビデオを検索
-
-
3:40 - Add reorderable to the preview
#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) } -
4:40 - Add reorder container to the GameView
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() } } -
5:58 - Add reorderable to PileView
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 } } -
7:50 - Add dragContainer to customize the reorderContainer modifier.
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() } } -
8:45 - Add dragPreviewsFormation to customize how the dragged cards appear
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() } } -
9:14 - Add dropPreviewsFormation to customize how dragged cards appear over a destination
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() } } -
11:40 - Add a drag configuration to allow move.
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)) } } -
12:05 - Add a drop destination modifier and configure it
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() } }
-
-
- 0:00 - Introduction
SwiftUI's expanded drag and drop in the 2027 releases — reorderable views, multi-item drags, and drag configuration — previewed through the Solitaire game used throughout the code-along.
- 1:42 - Reordering
Adopt the new reorderable and reorderContainer modifiers to let people rearrange content with drag and drop. Demonstrated by enabling card reordering across all piles in a Solitaire app and excluding face-down cards from the interaction.
- 6:50 - Drag multiple items
Use the drag container API to lift several items at once based on a selection. Customize how previews appear during the drag and at the drop destination with dragPreviewsFormation and dropPreviewsFormation — shown picking up and stacking multiple Solitaire cards.
- 9:59 - Drag configuration
Express intent for how data transfers between a drag source and a drop destination. Use dragConfiguration to specify move (vs. copy) on the source, and dropConfiguration on the destination to have the final say — used to move a card from the deck into a pile without duplication.
- 14:29 - Next steps
Recap: make your content reorderable, allow people to drag multiple items at once, and express intent with drag and drop configurations.