{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-271", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/271", "wordCount": 2288, "segments": [{"start": 7.0, "text": "Hi, I'm Jack, an engineer on the UI Frameworks team."}, {"start": 11.0, "text": "In this video, I'd like to share with you some of the new drag and drop APIs"}, {"start": 15.0, "text": "available in the 2027 releases."}, {"start": 19.0, "text": "Since iOS 16, SwiftUI has provided drag and drop"}, {"start": 23.0, "text": "through the draggable and dropDestination modifiers."}, {"start": 26.0, "text": "The draggable modifier allows people to move content, like photos and text,"}, {"start": 31.0, "text": "throughout the system by using a drag gesture."}, {"start": 34.0, "text": "You can conform your data to the Transferable protocol,"}, {"start": 38.0, "text": "giving it a transfer representation that allows it to be dragged through the system"}, {"start": 42.0, "text": "and accepted by other apps."}, {"start": 44.0, "text": "For example, I can conform my card type to Transferable"}, {"start": 48.0, "text": "and then pass an instance of it to the draggable modifier."}, {"start": 52.0, "text": "The dropDestination modifier"}, {"start": 54.0, "text": "allows your app and views to accept all kinds of content."}, {"start": 58.0, "text": "You decide what data your view can handle"}, {"start": 60.0, "text": "by specifying a Transferable type to accept."}, {"start": 63.0, "text": "If I want my view to accept card instances, for example,"}, {"start": 67.0, "text": "I can provide the card type to the dropDestination modifier."}, {"start": 71.0, "text": "For more information about how to make your content Transferable,"}, {"start": 75.0, "text": "I'd recommend watching the video \"Meet Transferable\" from WWDC 2022."}, {"start": 81.0, "text": "SwiftUI has expanded the capabilities of drag and drop in three major ways."}, {"start": 86.0, "text": "There's a new reordering API,"}, {"start": 88.0, "text": "that allows people to rearrange content using drag and drop."}, {"start": 92.0, "text": "You can enable people to drag multiple items at a time"}, {"start": 95.0, "text": "using the Drag Container APIs."}, {"start": 97.0, "text": "And you can now configure how data is transferred"}, {"start": 100.0, "text": "in drags and at dropDestinations."}, {"start": 102.0, "text": "I'll be implementing the drag and drop interactions for a game of Solitaire."}, {"start": 107.0, "text": "The game will look similar to how I've arranged these playing cards."}, {"start": 110.0, "text": "I can move cards between the piles,"}, {"start": 115.0, "text": "and I can pull a card from the deck of the remaining cards."}, {"start": 120.0, "text": "You don't need to be familiar with Solitaire"}, {"start": 122.0, "text": "to understand the API I'll be using."}, {"start": 124.0, "text": "If you'd like to follow along, you can download the sample project."}, {"start": 128.0, "text": "It contains the views and game logic needed to get started."}, {"start": 131.0, "text": "I'll start by adopting the new reorderable API in my app."}, {"start": 134.0, "text": "Just like the cards in my Solitaire game,"}, {"start": 137.0, "text": "your app's content likely exists in some order."}, {"start": 140.0, "text": "But people might want to change that order to organize their content"}, {"start": 143.0, "text": "in the way that works best for them."}, {"start": 146.0, "text": "When I make the cards in the Solitaire game reorderable,"}, {"start": 149.0, "text": "I'll be able to drag them individually."}, {"start": 152.0, "text": "When I drag a view,"}, {"start": 153.0, "text": "it'll be lifted from its position in the view hierarchy"}, {"start": 156.0, "text": "and an empty placeholder will take its spot."}, {"start": 159.0, "text": "From there, I can drag this card throughout my app."}, {"start": 162.0, "text": "As I move the card over other cards,"}, {"start": 165.0, "text": "they make space for me to drop the one that I'm dragging."}, {"start": 169.0, "text": "The placeholder updates to reflect where the dragged card will go once I drop it."}, {"start": 173.0, "text": "And when I drop the card, it will be moved to its new position."}, {"start": 177.0, "text": "I'll add this capability to my Solitaire game."}, {"start": 180.0, "text": "I'll first implement reordering without the game rules in place."}, {"start": 184.0, "text": "Then, once everything's working,"}, {"start": 187.0, "text": "I'll refine the implementation to include the rules."}, {"start": 191.0, "text": "I've opened the Xcode project for this app."}, {"start": 193.0, "text": "Its contents are split into two main folders: Game and Views."}, {"start": 199.0, "text": "The Game folder contains the SwiftData models and code for updating them."}, {"start": 203.0, "text": "I'm going to be spending most of my time in the Views folder, where the SwiftUI is."}, {"start": 209.0, "text": "I'll open to GameView, which contains the layout and views of the playing area."}, {"start": 214.0, "text": "But before I add reordering to my app, I'm going to test it out in a Preview."}, {"start": 220.0, "text": "At the bottom of this file,"}, {"start": 222.0, "text": "I have one that shows four cards on a green background."}, {"start": 226.0, "text": "To enable reordering,"}, {"start": 227.0, "text": "I add the reorderable modifier to the ForEach that creates the cards."}, {"start": 234.0, "text": "Then, I put a reorderContainer modifier on the HStack."}, {"start": 241.0, "text": "I specify that the item type of the container is CardValue,"}, {"start": 245.0, "text": "which matches the type used in my ForEach view."}, {"start": 249.0, "text": "In the closure, I'm provided a difference at the end of an operation,"}, {"start": 253.0, "text": "and I handle it by updating my cards array."}, {"start": 257.0, "text": "Now, I can interact with the Preview and reorder cards."}, {"start": 261.0, "text": "I can drag the ace of clubs from the left and drop it at the end."}, {"start": 267.0, "text": "In Solitaire, my goal is to organize cards by moving them between the piles."}, {"start": 272.0, "text": "So in my app, I want all of the piles to be in the same reorderContainer."}, {"start": 278.0, "text": "I can accomplish this by adding the reorderContainer modifier"}, {"start": 282.0, "text": "to the HStack that contains the piles."}, {"start": 287.0, "text": "I provide the same CardValue type for the container's item type."}, {"start": 291.0, "text": "Because there are multiple piles,"}, {"start": 293.0, "text": "I need a way to uniquely refer to each of them."}, {"start": 296.0, "text": "I use the Card.Group type to identify each pile, and,"}, {"start": 300.0, "text": "in the closure, I handle a difference across multiple piles."}, {"start": 306.0, "text": "Lastly, I need to go to PileView and add the reorderable modifier."}, {"start": 314.0, "text": "Because I have multiple reorderable modifiers in the same container,"}, {"start": 318.0, "text": "I need to provide a unique identifier for each one."}, {"start": 321.0, "text": "Now, I can drag the four of diamonds"}, {"start": 323.0, "text": "and drop it onto the pile with the five of spades."}, {"start": 329.0, "text": "This is great, but there's one more refinement I'd like to make right now."}, {"start": 333.0, "text": "In Solitaire, you can't reorder the face down cards."}, {"start": 337.0, "text": "But right now, I'm able to lift one from a pile."}, {"start": 340.0, "text": "This is because I made the entire array of cards in PileView reorderable."}, {"start": 345.0, "text": "While I could use some advanced drag and drop API to control this,"}, {"start": 349.0, "text": "there's an even easier way to solve this."}, {"start": 353.0, "text": "I can add a second ForEach view without the reorderable modifier"}, {"start": 358.0, "text": "and slice the cards array between the two."}, {"start": 362.0, "text": "The first ForEach view contains all of the face down cards."}, {"start": 367.0, "text": "The second, with the reorderable modifier, contains all of the face up cards."}, {"start": 373.0, "text": "With this change, I can still reorder the face up cards,"}, {"start": 379.0, "text": "but when I drag on a face down card, nothing happens."}, {"start": 384.0, "text": "Now, I have the basic interactions of Solitaire in play."}, {"start": 388.0, "text": "Because of the reorderable modifier,"}, {"start": 389.0, "text": "I can now drag individual cards around to reorder them."}, {"start": 393.0, "text": "The reorderContainer modifier"}, {"start": 395.0, "text": "allowed me to scope reordering to include all of the piles."}, {"start": 399.0, "text": "And by moving the face down cards into a separate ForEach,"}, {"start": 402.0, "text": "I was able to keep them from being reordered."}, {"start": 405.0, "text": "These APIs are newly available on all Apple platforms that support drag and drop."}, {"start": 410.0, "text": "But my Solitaire game is still missing a critical part of the gameplay,"}, {"start": 414.0, "text": "being able to move multiple cards at once."}, {"start": 417.0, "text": "In Solitaire, when I drag a card in the middle of a pile,"}, {"start": 420.0, "text": "it brings the cards stacked on it."}, {"start": 423.0, "text": "But in your app, the interaction model might not be as straightforward."}, {"start": 428.0, "text": "One way to handle this is to add selection to your draggable items."}, {"start": 432.0, "text": "In this example, I'll use a simple tap-to-select interaction model."}, {"start": 437.0, "text": "As I tap each card, it's added to the selection."}, {"start": 442.0, "text": "Once I perform a drag gesture on one of the selected cards,"}, {"start": 445.0, "text": "all three cards lift together."}, {"start": 449.0, "text": "I'll add the ability to drag multiple cards at once to my Solitaire game."}, {"start": 454.0, "text": "I'll open back up to GameView,"}, {"start": 456.0, "text": "where I added the reorderContainer modifier."}, {"start": 459.0, "text": "Reorder containers implicitly provide their own dragContainer"}, {"start": 463.0, "text": "and dropDestination capabilities,"}, {"start": 466.0, "text": "but I can add my own to customize this behavior."}, {"start": 469.0, "text": "I declare the dragContainer modifier below the reorderContainer modifier."}, {"start": 475.0, "text": "For them to work together,"}, {"start": 476.0, "text": "I need to make sure that they use the same type, CardValue."}, {"start": 481.0, "text": "In the closure, I'm given an item identifier,"}, {"start": 484.0, "text": "and I need to provide transferable data for the items that I want to move."}, {"start": 490.0, "text": "I call out to my game logic"}, {"start": 492.0, "text": "to find the cards stacked above the one I'm trying to drag."}, {"start": 497.0, "text": "When I drag this four of clubs, which is below a three of diamonds,"}, {"start": 501.0, "text": "I now get both cards in the same drag."}, {"start": 507.0, "text": "When I dragged the stack of cards together,"}, {"start": 509.0, "text": "they collapsed into a pile, with the first card on top."}, {"start": 513.0, "text": "This is the default preview, but I can configure it to several options,"}, {"start": 517.0, "text": "including pile, list, and stack."}, {"start": 520.0, "text": "I can configure this with the dragPreviewsFormation modifier."}, {"start": 525.0, "text": "I choose stack because it's compact and has the feel of stacked cards."}, {"start": 530.0, "text": "When I drag the four of clubs now, the cards form into a neat stack."}, {"start": 535.0, "text": "But when I drag over the piles,"}, {"start": 536.0, "text": "they revert back to the default appearance."}, {"start": 540.0, "text": "Because they are above my reorderContainer's dropDestination,"}, {"start": 543.0, "text": "they're using its drop formation."}, {"start": 546.0, "text": "To configure that, I can use the dropPreviewsFormation modifier."}, {"start": 551.0, "text": "Because I want this to be consistent across all dropDestinations in my app,"}, {"start": 556.0, "text": "I declare this modifier on the root layout of my GameView."}, {"start": 560.0, "text": "Now, the cards I drag maintain the same appearance throughout the playing area."}, {"start": 567.0, "text": "I made it possible to drag multiple cards at a time in my Solitaire game."}, {"start": 572.0, "text": "I used the Drag Container API to make it possible to lift more than one at a time."}, {"start": 577.0, "text": "I added dragPreviewsFormation to customize the appearance of the lifted cards,"}, {"start": 582.0, "text": "and I ensured a consistent appearance"}, {"start": 584.0, "text": "by setting the same value for dropPreviewsFormation."}, {"start": 589.0, "text": "The dragContainer modifier is newly available on iOS, iPadOS, and visionOS 27."}, {"start": 595.0, "text": "All three modifiers are available on macOS 26 and newer."}, {"start": 599.0, "text": "To implement the remaining parts of the game,"}, {"start": 601.0, "text": "I'll use the new Drag Configuration API."}, {"start": 605.0, "text": "When adding a drag capability to my card view,"}, {"start": 608.0, "text": "I should think about how I want that card's value to move."}, {"start": 611.0, "text": "In my Solitaire game, I'll be dragging new cards into the piles."}, {"start": 616.0, "text": "By default, SwiftUI will suggest that the data moves by copy."}, {"start": 621.0, "text": "This works well when you're moving data between apps or inserting something new."}, {"start": 627.0, "text": "But in my card game,"}, {"start": 628.0, "text": "dragging a card should not create a copy at the destination."}, {"start": 632.0, "text": "Instead, I'd like the card to be moved from the deck into the piles."}, {"start": 638.0, "text": "Unlike my reorderContainer, which is designed to handle moves,"}, {"start": 642.0, "text": "these Views are a separate drag source and dropDestination."}, {"start": 646.0, "text": "I'll need to use the new Drag Configuration API to achieve this."}, {"start": 650.0, "text": "I'll add the ability for cards to be moved into the piles without duplication"}, {"start": 654.0, "text": "by using the Drag Configuration API."}, {"start": 658.0, "text": "In the app, I have the deck of remaining cards on the top left of the playing area."}, {"start": 663.0, "text": "I want to be able to drag this six of diamonds"}, {"start": 666.0, "text": "onto the pile with the seven of spades."}, {"start": 670.0, "text": "To get started, I open Xcode to RemainderView,"}, {"start": 676.0, "text": "which contains the views for this part of the game."}, {"start": 679.0, "text": "The current face up card already has its own drag modifiers."}, {"start": 684.0, "text": "By default, this view will transfer the value by copy."}, {"start": 688.0, "text": "I add the dragConfiguration modifier to this view"}, {"start": 692.0, "text": "and specify that my intent is for this card to be transferred by move."}, {"start": 698.0, "text": "But it's the dropDestination that decides how the data is transferred."}, {"start": 703.0, "text": "In this case, my dropDestination is the reorderContainer in GameView."}, {"start": 708.0, "text": "By default, the reorderContainer only accepts moves within the container."}, {"start": 712.0, "text": "If I want to accept new items, I have to provide my own dropDestination modifier."}, {"start": 718.0, "text": "I add one below the dragContainer,"}, {"start": 722.0, "text": "and I specify that I want to accept the same card type."}, {"start": 727.0, "text": "I read the reorderContainer's destination value for the inserted items,"}, {"start": 731.0, "text": "and if it exists,"}, {"start": 733.0, "text": "I call into game logic to insert the newCards at the destination."}, {"start": 739.0, "text": "Moves between piles are still handled by the closure"}, {"start": 742.0, "text": "on the reorderContainer modifier."}, {"start": 747.0, "text": "With the dropDestination in place, I can now configure how it receives items."}, {"start": 752.0, "text": "I add the dropConfiguration modifier,"}, {"start": 755.0, "text": "which has the final say about how the data is transferred."}, {"start": 759.0, "text": "I'm provided session information in the closure,"}, {"start": 763.0, "text": "and I can use that to return a dropConfiguration"}, {"start": 765.0, "text": "that tells the dropDestination how to accept the card."}, {"start": 771.0, "text": "I do three things in this closure."}, {"start": 773.0, "text": "First, I determine which pile should receive the cards"}, {"start": 777.0, "text": "by checking where the drag is."}, {"start": 779.0, "text": "I create a destination value with that pile's identifier to tell SwiftUI"}, {"start": 783.0, "text": "where the cards should go."}, {"start": 786.0, "text": "Second, I express my intent to only support move-based transfers."}, {"start": 792.0, "text": "Normally, you'd want to support copying as a fallback when move isn't available,"}, {"start": 797.0, "text": "but in a card game, copying doesn't make sense."}, {"start": 801.0, "text": "And third,"}, {"start": 802.0, "text": "I validate that this destination value is allowed within the rules of the game."}, {"start": 807.0, "text": "If I return a forbidden operation,"}, {"start": 810.0, "text": "SwiftUI will prevent the dropDestination from receiving the cards."}, {"start": 816.0, "text": "With all of those changes, now I'm ready to drag a card into play."}, {"start": 821.0, "text": "I can drag this six of diamonds from the remainder deck"}, {"start": 824.0, "text": "onto the pile with the seven of spades, only because the rules allow me to."}, {"start": 828.0, "text": "And when I do, the six of diamonds is moved from the remainder's deck to the pile."}, {"start": 834.0, "text": "If I try to drop this queen of diamonds on the pile that has the seven of diamonds,"}, {"start": 838.0, "text": "the queen will return to the remainder deck"}, {"start": 840.0, "text": "because the move is not valid."}, {"start": 844.0, "text": "I was able to drag cards into piles using the new configuration modifiers."}, {"start": 849.0, "text": "I started by adding a dragConfiguration on my source card,"}, {"start": 852.0, "text": "where I specified my intent to transfer the card by move."}, {"start": 856.0, "text": "Then, I used dropDestination to enable the reorderContainer to accept new cards."}, {"start": 863.0, "text": "And I applied dropConfiguration to that destination"}, {"start": 866.0, "text": "so that it would take cards by move when the play was allowed."}, {"start": 869.0, "text": "I started building this app with only a reorderable and reorderContainer modifier."}, {"start": 875.0, "text": "But I was able to fully customize reordering"}, {"start": 877.0, "text": "by composing drag and drop modifiers on top of them."}, {"start": 880.0, "text": "Now, I have a fully complete game of Solitaire."}, {"start": 884.0, "text": "Even if you're not building Solitaire,"}, {"start": 886.0, "text": "you can use these APIs to make your apps better."}, {"start": 889.0, "text": "Consider giving people the ability to reorder the content in your app."}, {"start": 894.0, "text": "Remember that you can also give them the ability"}, {"start": 896.0, "text": "to drag multiple items at a time with the Drag Container API."}, {"start": 900.0, "text": "And fine-tuning your app with drag and drop configurations"}, {"start": 903.0, "text": "will make it a delight to use."}, {"start": 905.0, "text": "Now, if you'll excuse me, I have to get back to work!"}, {"start": 911.0, "text": "Thanks for watching!"}]}