View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 머신 러닝 및 AI
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 주도 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • App Store Connect 도움말
    • 새로 추가될 요구 사항
    • 계약 및 지침
    • 시스템 상태
  • 빠른 링크

    • 이벤트
    • 뉴스
    • 포럼
    • 샘플 코드
    • 비디오
 

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 전체 비디오
  • 소개

더 많은 비디오

  • 소개
  • 요약
  • 코드
  • 코딩 실습: SwiftUI에서 강력한 드래그 앤 드롭 빌드하기

    SwiftUI의 최신 드래그 앤 드롭 기능을 살펴보기 위한 Solitaire 게임 빌드 실습을 함께 해 보세요. 새로운 리오더 API를 사용하여 사용자들이 콘텐츠를 배열하고, 드래그 컨테이너를 구현하여 여러 항목을 한 번에 이동하며, 앱 규칙에 맞게 드래그 앤 드롭 수명 주기를 맞춤 설정할 수 있도록 하는 방법을 안내합니다. 이 세션을 최대한 활용하려면 WWDC22의 ‘Transferable 소개'를 시청하세요.

    챕터

    • 0:00 - Introduction
    • 1:42 - Reordering
    • 6:50 - Drag multiple items
    • 9:59 - Drag configuration
    • 14:29 - Next steps

    리소스

    • Making a card game with drag, drop, and reordering in SwiftUI
    • Drag and drop
      • HD 비디오
      • SD 비디오

    관련 비디오

    WWDC22

    • Transferable 소개
  • 비디오 검색…
    • 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.

Developer Footer

  • 비디오
  • WWDC26
  • 코딩 실습: SwiftUI에서 강력한 드래그 앤 드롭 빌드하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • Apple Intelligence
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, 식별자 및 프로파일(영문)
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(영문)
    • Mini Apps Partner Program
    • News Partner Program(영문)
    • Video Partner Program(영문)
    • Security Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    최신 뉴스 읽기.
    Apple Developer 앱 받기.
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침