Mac 앱의 두 패널 사이에서 합성 도형을 복사하려면 타입 지정 SwiftUI 드래그 앤 드롭으로 시작하세요. 기존 제공자 인터페이스가 있거나 이전 macOS 대상으로 인해 필요한 경우 아이템 제공자를 선택하세요. 어느 API를 선택하든 먼저 페이로드, 허용 규칙, 모델 소유자를 정의하세요.
성공적인 드롭의 의미 정의하기
여기서 독자가 수행할 작업은 구체적입니다. Mac 앱 안의 팔레트에서 합성 원이나 사각형을 같은 앱의 컬렉션으로 드래그할 수 있게 합니다. 대상은 새 항목을 만들고 팔레트는 그대로 둡니다. 모든 데이터는 메모리에 있으며 창을 닫으면 컬렉션이 사라집니다.
Apple은 Swift 드래그 앤 드롭에 transferable 항목을 권장합니다. 문서에서는 제공자 기반 수정자도 지원하므로, 어떤 방식을 선택할지는 배포 요구 사항과 통합해야 하는 인터페이스에 달려 있습니다. 드래그 앤 드롭 — Apple.
뷰를 구현하기 전에 전송 계약을 작성하세요.
| 계약 질문 | 이 예시의 결정 |
|---|---|
| 경계를 넘어 전달되는 것은 무엇인가요? | 도형 종류를 설명하는 버전 지정 값 |
| 누가 표현을 받을 수 있나요? | 원본 앱만 |
| 드롭이 성공하면 어떤 일이 일어나나요? | 새 식별자가 부여된 컬렉션 항목 하나를 추가 |
| 누가 변경을 소유하나요? | 대상 뷰의 컬렉션 상태 |
| 취소하거나 거부하면 어떻게 되나요? | 모델 변경 없음 |
| 드롭 위치는 무엇을 의미하나요? | 여기서는 의미 없음. 허용된 항목은 순서대로 추가 |
이는 애플리케이션 설계 결정입니다. 드래그 미리보기는 페이로드를 보여 주고, 대상은 그 페이로드가 모델을 어떻게 변경할지 결정합니다. 이러한 책임을 분리하면 컬렉션 규칙을 다시 작성하지 않고도 전송 방식을 바꿀 수 있습니다.
계약을 표현하는 최소 API 선택하기
| 상황 | 시작점 | 이유 |
|---|---|---|
| SwiftUI 소스와 대상, macOS 13 이상 | Transferable, .draggable, 타입 지정 .dropDestination |
허용되는 모델 타입을 직접 표현 |
기존 NSItemProvider 통합 |
.onDrag 및 .onDrop |
명시적인 표현 등록과 로드 유지 |
| macOS 13 이전 대상 | 해당 대상에서 제공자 수정자를 사용할 수 있는지 확인 | 타입 지정 기본 방식에는 macOS 13이 필요하고, 아래에서 설명하는 제공자 오버로드에는 macOS 11이 필요 |
| 기존 AppKit 뷰가 마우스 상호작용을 소유 | AppKit 드래그 API | 뷰의 이벤트 처리와 드래그 세션 수명 주기 유지 |
Apple은 macOS 13부터 Transferable과 원래의 타입 지정 드롭 대상을 문서화합니다. onDrop(of:isTargeted:perform:)의 제공자 기반 오버로드 중 UTType 값 배열을 받는 것은 macOS 11부터 사용할 수 있습니다. 모든 드래그 앤 드롭 API가 동일한 최소 버전을 요구한다고 가정하지 말고, 사용하려는 특정 오버로드를 확인하세요. Transferable, dropDestination(for:action:isTargeted:), onDrop(of:isTargeted:perform:).
두 접근 방식 모두 자체 앱 내 공개 범위를 지원합니다. 타입 지정 표현에는 .visibility(.ownProcess)를 적용하세요. Apple은 이 공개 범위를 소스 앱 내부에서만 사용할 수 있는 것으로 정의합니다. 따라서 전송을 앱 내부로 제한하는 것만을 이유로 아이템 제공자를 선택할 필요는 없습니다. visibility(_:), ownProcess.
타입 지정 복사 상호작용 구현하기
이 완전한 예시는 새 macOS SwiftUI 앱 대상의 Swift 파일 두 개로 구성됩니다. 배포 대상을 macOS 13 이상으로 설정하고, 이 기본 구현에는 Swift 5 언어 모드를 사용하세요. 생성된 앱 진입점을 두 번째 파일로 교체하고 @main 선언은 하나만 남기세요. 코드는 검토했지만, 사용할 수 있는 셸에 Swift 툴체인이 없어 여기서 컴파일하지는 않았습니다.
페이로드와 콘텐츠 타입 선언하기
대상의 정보 프로퍼티 목록에 내보낸 타입 선언을 추가하세요. Xcode의 프로퍼티 목록 편집기에서 UTExportedTypeDeclarations를 항목 하나가 들어 있는 배열로 만들고, 그 항목에 다음 값을 지정하세요. UTTypeIdentifier는 com.example.shape-transfer, UTTypeDescription은 Synthetic Shape Transfer, UTTypeConformsTo는 public.data를 포함하는 배열입니다. 이 식별자는 예시일 뿐입니다. 실제 앱에는 고유한 식별자를 사용하세요.
Swift 선언과 프로퍼티 목록의 식별자는 일치해야 합니다. Apple은 사용자 지정 uniform type에 대응하는 프로퍼티 목록 항목이 필요하다고 설명하며, 앱이 정의하는 형식에 대한 내보내기 선언도 설명합니다. 이 예시는 파일이 아닌 데이터를 전송하므로 파일 이름 확장자는 추가하지 않습니다. 앱의 파일 및 데이터 타입 정의하기.
ShapePayload.swift를 만드세요.
import Foundation
import CoreTransferable
import UniformTypeIdentifiers
extension UTType {
static let shapeTransfer = UTType(
exportedAs: "com.example.shape-transfer",
conformingTo: .data
)
}
enum ShapeKind: String, Codable, Sendable {
case circle = "Circle"
case square = "Square"
}
struct ShapePayload: Codable, Sendable, Transferable {
let version: Int
let kind: ShapeKind
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .shapeTransfer)
.visibility(.ownProcess)
}
}
struct PlacedShape: Identifiable {
let id = UUID()
let kind: ShapeKind
}
CodableRepresentation은 페이로드와 전송 가능한 데이터 간 변환을 제공합니다. 페이로드에는 뷰나 변경 가능한 컬렉션 참조가 아니라 값이 들어갑니다. 명시적인 Sendable 준수는 전송 가능한 구조체의 저장된 멤버도 모두 전송 가능해야 한다는 Swift 규칙을 따릅니다. Transferable, Sendable.
의도적으로 표현은 하나만 둡니다. 텍스트 프록시를 추가하면 항목을 사용하는 다른 방법이 생기지만, 이 계약에는 쓸모가 없습니다. Apple은 표현 순서와 대체 형식을 문서화합니다. 실제 수신자에게 필요할 때만 추가하세요. 뷰를 드래그 소스로 만들기.
페이로드를 허용하고 컬렉션을 한 번 갱신하기
ShapeTransferApp.swift를 만드세요.
import SwiftUI
@main
struct ShapeTransferApp: App {
var body: some Scene {
WindowGroup {
ShapeTransferView()
}
}
}
@MainActor
struct ShapeTransferView: View {
@State private var placed: [PlacedShape] = []
@State private var targeted = false
@State private var message = "Drag a shape into Collection."
var body: some View {
HStack(alignment: .top, spacing: 24) {
VStack(spacing: 16) {
Text("Palette").font(.headline)
paletteItem(.circle)
paletteItem(.square)
}
.frame(width: 160)
VStack(alignment: .leading, spacing: 12) {
Text("Collection").font(.headline)
Text(message)
ForEach(placed) { item in
Label(item.kind.rawValue,
systemImage: symbol(for: item.kind))
}
Spacer(minLength: 20)
}
.padding()
.frame(width: 300, height: 320, alignment: .topLeading)
.background(targeted ? Color.blue.opacity(0.15)
: Color.gray.opacity(0.08))
.contentShape(Rectangle())
.dropDestination(
for: ShapePayload.self,
action: { payloads, _ in
guard !payloads.isEmpty,
payloads.allSatisfy({ $0.version == 1 })
else {
message = "Drop rejected: unsupported payload."
return false
}
let additions = payloads.map {
PlacedShape(kind: $0.kind)
}
placed.append(contentsOf: additions)
message = "Added \(additions.count) shape(s)."
return true
},
isTargeted: { targeted = $0 }
)
}
.padding(24)
}
private func paletteItem(_ kind: ShapeKind) -> some View {
Label(kind.rawValue, systemImage: symbol(for: kind))
.padding()
.background(Color.gray.opacity(0.12))
.draggable(ShapePayload(version: 1, kind: kind))
}
private func symbol(for kind: ShapeKind) -> String {
switch kind {
case .circle: return "circle.fill"
case .square: return "square.fill"
}
}
}
action: 및 isTargeted: 레이블을 명시하면 원래의 타입 지정 대상 오버로드가 선택됩니다. Apple은 반환되는 불리언 값을 성공 또는 실패로, 위치 인수를 대상 뷰의 좌표 공간에 있는 값으로 정의합니다. 여기서는 컬렉션이 도형을 배치하지 않고 추가하므로 위치를 의도적으로 사용하지 않습니다. dropDestination(for:action:isTargeted:).
허용 규칙은 아무것도 추가하기 전에 전달된 모든 페이로드를 확인합니다. 지원되지 않는 버전이면 전달된 배치 전체를 거부하고, 유효한 페이로드마다 대상에서 새 식별자를 부여합니다. 팔레트에서 같은 드래그를 반복하면 사본이 하나 더 만들어집니다. 이 규칙은 클로저에 전달된 값을 다룹니다. 디코딩 오류는 ShapePayload 값이 되기 전에 발생합니다.
대상 지정 상태는 표시 방식만 바꿉니다. 항목을 삽입하거나 소스에서 제거하지 않습니다. 이러한 분리 덕분에 취소의 결과도 단순합니다. 드롭이 허용되지 않으면 컬렉션은 그대로 유지됩니다.
통합에 필요한 경우 아이템 제공자 사용하기
제공자 구현도 동일한 버전과 도형 종류를 전달할 수 있습니다. 계약은 그대로 유지하고 다음 단계에 따라 전송 방식만 바꾸세요.
.onDrag클로저에서NSItemProvider를 만들고.ownProcess공개 범위로 동일한 사용자 지정 타입 식별자에 데이터 표현을 등록합니다.- 변경할 수 없는 페이로드 값이나 준비된 바이트를 로드 핸들러에서 캡처합니다. 해당 값을 인코딩하고 완료 핸들러에 데이터 또는 오류를 전달합니다.
.onDrop이 사용자 지정 타입을 허용하도록 설정합니다.- 드롭 동작 안에서
loadDataRepresentation(forTypeIdentifier:completionHandler:)를 사용해 허용된 각 제공자의 로드를 즉시 시작합니다. - 반환된 데이터를 디코딩하고 버전을 검증한 뒤 결과를 모읍니다. 필요한 결과를 모두 사용할 수 있게 된 후에만 메인 액터에서 컬렉션을 변경합니다.
Apple은 제공자의 명시적 데이터 등록과 자체 앱 내 공개 범위를 문서화합니다. 로드 메서드는 비동기이며 진행 객체를 반환합니다. 일치하는 타입 식별자는 로드를 시도할 수 있음을 나타낼 뿐, 로드와 디코딩이 성공한다는 뜻은 아닙니다. registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:), NSItemProviderRepresentationVisibility.ownProcess, loadDataRepresentation(forTypeIdentifier:completionHandler:).
두 가지 시점 경계가 중요합니다. 첫째, Apple은 제공자 로드를 .onDrop 동작 안에서 시작해야 하지만 완료는 나중에 이루어져도 된다고 명시합니다. 초기 로드 호출을 다른 액터의 후속 작업으로 옮기면 문서화된 요구 사항을 위반합니다. 둘째, Apple은 표현 핸들러에서 내보내기 작업을 메인 액터나 메인 스레드로 전달하면 멈출 수 있다고 경고합니다. 내보내기 작업은 UI 변경과 분리하고, 가져오기가 끝난 뒤 컬렉션 상태를 갱신하세요. onDrop(of:isTargeted:perform:), onDrag(_:).
비동기 제공자 구현에서는 로드 작업을 수락하는 것과 모델 트랜잭션을 완료하는 것을 구분하세요. 인덱싱된 결과로 제공자 순서를 유지하고 로드 또는 디코딩 실패를 보고하세요. 계약상 모든 항목이 성공해야 한다면 컬렉션을 변경하지 마세요. 필터링 작업으로 실패한 항목을 조용히 버리지 마세요. 이는 예시의 배치 수락 정책에서 도출한 구현 규칙입니다.
AppKit 이벤트 소유권과 이동 의미 보존하기
기존 AppKit 뷰가 이미 마우스 상호작용을 소유한다면 세션 시작도 그곳에서 유지하세요. Apple의 beginDraggingSession(with:event:source:)는 시작 마우스 다운 이벤트, 해당 뷰의 좌표를 사용하는 드래그 항목 프레임, 그리고 NSDraggingSource를 준수하는 소스를 인수합니다. 소스 콜백은 세션 시작, 이동, 완료 또는 취소를 처리합니다. beginDraggingSession(with:event:source:).
AppKit의 표현 경계도 명시적입니다. NSDraggingItem(pasteboardWriter:)에는 NSPasteboardWriting 객체가 필요합니다. SwiftUI 제공자 콜백과 AppKit 페이스트보드 작성자는 서로 다른 인터페이스입니다. 사용자 지정 AppKit 뷰를 SwiftUI에 연결할 때 AppKit 뷰가 이벤트와 세션을 계속 담당하고, 공유 모델이 허용된 컬렉션 변경을 소유하도록 하세요. init(pasteboardWriter:).
이 예시는 복사를 수행합니다. 기존 항목을 컬렉션 간에 이동하도록 작업이 바뀌면 안정적인 항목 식별자를 추가하고 제거를 정확히 한 곳에서 소유하도록 정의하세요. 공유 저장소가 항목을 한 번 이동하거나, 대상이 삽입한 뒤 이동 완료가 확인되면 소스가 제거할 수 있습니다. 두 방식을 함께 사용하면 항목이 두 번 제거될 위험이 있습니다.
현재 Apple 문서에는 macOS 26용 DragConfiguration, 세션 기반 타입 지정 드롭 오버로드, 드래그 세션 관찰 기능도 포함되어 있습니다. 소스가 지원 작업을 제안하고, 대상이 작업을 선택하며, 세션 완료 시 소스가 결과에 대응할 수 있습니다. 이 방식을 적용하기 전에 각 기호의 SDK 가용성을 확인하세요. DragConfiguration, dropDestination(for:isEnabled:action:), onDragSessionUpdated(_:).
포인터가 대상에 들어오거나 미리보기가 나타났다는 이유로 소스 항목을 제거하지 마세요. 이러한 이벤트는 허용되기 전에 발생합니다. 자체 앱 내 이동에서 유용한 불변 조건은 트랜잭션 완료 후 논리적 항목 하나가 컬렉션 하나만 소유하는 것입니다.
관찰 가능한 결과로 계약 확인하기
Mac에서 구현을 확인하려면 다음 순서로 진행하세요.
Circle을Collection으로 드래그합니다. 대상에 원 하나가 추가되고 팔레트 항목은 그대로 유지됩니다.- 드래그를 반복합니다. 대상에 독립된 식별자를 가진 항목이 하나 더 추가됩니다.
Square를 대상 바깥으로 드래그한 뒤 놓습니다. 컬렉션은 바뀌지 않습니다.- 소스 페이로드 버전을 임시로
2으로 바꿉니다. 대상에서 거부되었음을 표시하고 아무것도 추가하지 않습니다. 이후1로 되돌립니다. - 제공자 구현에서는 인코딩 또는 로드 오류를 의도적으로 반환합니다. 일부만 추가하지 말고 실패를 표시합니다.
- 이동 구현에서는 드래그를 취소하고 원래 항목이 원래 컬렉션에 남아 있는지 확인합니다.
구체적인 호환성이나 통합 요구 사항으로 제공자 또는 AppKit이 필요한 경우가 아니라면 이 두 패널 간 복사 작업에는 타입 지정 SwiftUI를 선택하세요. 지속적으로 중요한 것은 계약입니다. 정의된 값이 경계를 넘고, 대상이 이를 검증하며, 모델 소유자 하나가 허용된 변경을 반영합니다.
출처
드래그 앤 드롭 — Apple.
Transferable — Apple.
뷰를 드래그 소스로 만들기 — Apple.
visibility(_:) — Apple.
ownProcess — Apple.
앱의 파일 및 데이터 타입 정의하기 — Apple.
Sendable — Apple.
onDrag(_:) — Apple.
onDrop(of:isTargeted:perform:) — Apple.
registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:) — Apple.
loadDataRepresentation(forTypeIdentifier:completionHandler:) — Apple.
init(pasteboardWriter:) — Apple.
DragConfiguration — Apple.
onDragSessionUpdated(_:) — Apple.