For copying synthetic shapes between two panes in a Mac app, start with typed SwiftUI drag and drop. Choose item providers when an existing provider interface or an earlier macOS target requires them. Define the payload, acceptance rules, and model owner before choosing either API.

Define what a successful drop means

The reader task here is specific: let someone drag a synthetic circle or square from a palette into a collection in the same Mac app. The destination creates a new item; the palette stays unchanged. Everything lives in memory, and closing the window discards the collection.

Apple recommends transferable items for Swift drag and drop. Its documentation also supports provider-based modifiers, so the choice depends on deployment requirements and the interface you need to integrate. Drag and drop — Apple.

Write the transfer contract before implementing the views:

Contract question Decision for this example
What crosses the boundary? A versioned value describing a shape kind
Who can receive the representation? The source app only
What does a successful drop do? Append one newly identified collection item
Who owns the mutation? The destination view’s collection state
What happens on cancellation or rejection? No model change
What does the drop location mean? Nothing here; accepted items append in order

These are application design decisions. A drag preview depicts the payload, while the destination decides how that payload changes the model. Keeping those responsibilities separate makes it possible to change the transport without rewriting the collection’s rules.

Choose the smallest API that expresses the contract

Situation Starting point Reason
SwiftUI source and destination, macOS 13 or later Transferable, .draggable, and typed .dropDestination Express the accepted model type directly
Existing NSItemProvider integration .onDrag and .onDrop Keep explicit representation registration and loading
A target earlier than macOS 13 Check provider modifier availability for that target The typed baseline requires macOS 13; the provider overload used below in discussion requires macOS 11
Existing AppKit view owns the mouse interaction AppKit dragging APIs Preserve the view’s event handling and dragging-session lifecycle

Apple documents Transferable and the original typed drop destination from macOS 13. The provider-based onDrop(of:isTargeted:perform:) overload accepting an array of UTType values starts at macOS 11. Check the particular overload rather than assuming that every drag-and-drop API shares one minimum version. Transferable, dropDestination(for:action:isTargeted:), onDrop(of:isTargeted:perform:).

Own-app visibility is available with both approaches. For a typed representation, apply .visibility(.ownProcess); Apple defines that level as visibility only within the source app. Choosing an item provider is therefore unnecessary merely to keep this transfer inside the app. visibility(_:), ownProcess.

Build the typed copy interaction

This complete example consists of two Swift files in a new macOS SwiftUI app target. Set the deployment target to macOS 13 or later and use Swift 5 language mode for this baseline. Replace the generated app entry point with the second file; keep only one @main declaration. The code has been inspected but was not compiled here because the available shell has no Swift toolchain.

Declare the payload and its content type

Add an exported type declaration to the target’s information property list. In Xcode’s property-list editor, create UTExportedTypeDeclarations as an array with one dictionary. Give that dictionary UTTypeIdentifier equal to com.example.shape-transfer, UTTypeDescription equal to Synthetic Shape Transfer, and UTTypeConformsTo as an array containing public.data. This identifier is illustrative; use a unique identifier for your actual app.

The Swift declaration and property-list identifier must agree. Apple requires corresponding property-list entries for custom uniform types and explains exported declarations for formats an app defines. This example transfers data rather than files, so it adds no filename extension. Defining file and data types for your app.

Create 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 supplies the conversion between the payload and transferable data. The payload contains a value, rather than a view or a mutable collection reference. Its explicit Sendable conformance follows Swift’s rule that a sendable structure’s stored members must themselves be sendable. Transferable, Sendable.

There is deliberately one representation. A text proxy would add another way to consume the item, which serves no purpose for this contract. Apple documents representation ordering and fallback formats; add those only when a receiver actually needs them. Making a view into a drag source.

Accept the payload and commit one collection update

Create 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"
        }
    }
}

The explicit action: and isTargeted: labels select the original typed destination overload. Apple defines its Boolean result as success or failure and its location argument in the destination view’s coordinate space. Here the location is intentionally unused because the collection appends rather than positions shapes. dropDestination(for:action:isTargeted:).

The acceptance rule checks every delivered payload before appending anything. An unsupported version rejects the delivered batch; valid payloads each receive a fresh destination identity. Repeating the same palette drag therefore makes another copy. This rule covers values delivered to the closure; decoding failures occur before they become ShapePayload values.

The targeting state changes presentation only. It does not insert items or remove anything from the source. That separation also gives cancellation a simple outcome: without an accepted drop, the collection remains unchanged.

Use item providers when the integration requires them

A provider implementation can carry the same version and shape kind. Keep the contract unchanged and replace the transport through these steps:

  1. In the .onDrag closure, create an NSItemProvider and register a data representation under the same custom type identifier, using .ownProcess visibility.
  2. Capture an immutable payload value or prepared bytes in its load handler. Encode that value and invoke the representation completion handler with either data or an error.
  3. Configure .onDrop to accept the custom type.
  4. Inside the drop action, start loading each accepted provider immediately with loadDataRepresentation(forTypeIdentifier:completionHandler:).
  5. Decode the returned data, validate the version, and collect results. Perform the eventual collection mutation on the main actor only after the required results are available.

Apple documents explicit data registration and own-app visibility for providers. Its loading method is asynchronous and returns a progress object. A matching type identifier establishes that loading can be attempted; it does not establish that loading and decoding will succeed. registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:), NSItemProviderRepresentationVisibility.ownProcess, loadDataRepresentation(forTypeIdentifier:completionHandler:).

Two timing boundaries matter. First, Apple says provider loading must begin within the .onDrop action, although completion may happen later. Moving the initial load call into a later task on another actor violates that documented requirement. Second, Apple warns against dispatching export work to the main actor or main thread from representation handlers because this can hang. Keep exporting independent of UI mutation; update collection state after importing completes. onDrop(of:isTargeted:perform:), onDrag(_:).

For an asynchronous provider implementation, distinguish accepting the loading work from completing the model transaction. Preserve provider order with indexed results, report loading or decoding failures, and leave the collection unchanged if your contract requires every item to succeed. Avoid silently discarding failed items with a filtering operation. These are implementation rules derived from the example’s batch acceptance policy.

Preserve AppKit event ownership and move semantics

If an existing AppKit view already owns the mouse interaction, keep session initiation there. Apple’s beginDraggingSession(with:event:source:) takes the originating mouse-down event, dragging items whose frames use that view’s coordinates, and a source conforming to NSDraggingSource. Source callbacks cover session start, movement, and completion or cancellation. beginDraggingSession(with:event:source:).

The AppKit representation boundary is explicit too: NSDraggingItem(pasteboardWriter:) requires an NSPasteboardWriting object. A SwiftUI provider callback and an AppKit pasteboard writer are different interfaces. When bridging a custom AppKit view into SwiftUI, let the AppKit view retain responsibility for its event and session; let the shared model own accepted collection changes. init(pasteboardWriter:).

The example performs copies. If the task changes to moving existing items between collections, add a stable item identifier and define exactly one owner of removal. A shared store can relocate an item once, or a destination can insert while the source removes after confirmed move completion. Combining both strategies risks removing the item twice.

Current Apple documentation includes DragConfiguration, a session-based typed drop overload, and drag-session observation for macOS 26. The source proposes supported operations, the destination chooses the operation, and session completion lets the source respond to the result. Check each symbol’s SDK availability before adopting this path. DragConfiguration, dropDestination(for:isEnabled:action:), onDragSessionUpdated(_:).

Do not remove a source item when a pointer enters the destination or when a preview appears. Those events precede acceptance. For an own-app move, the useful invariant is that one logical item has one owning collection after the transaction completes.

Verify the contract with observable outcomes

Use this sequence to check the implementation on a Mac:

  • Drag Circle into Collection: the destination gains one circle and the palette retains its entry.
  • Repeat the drag: the destination gains a second independently identified item.
  • Drag Square outside the destination and release: the collection does not change.
  • Temporarily change the source payload version to 2: the destination reports rejection and adds nothing; restore 1 afterward.
  • For a provider implementation, deliberately return an encoding or loading error: show failure without inserting a partial batch.
  • For a move implementation, cancel a drag and confirm that the original item still belongs to its original collection.

Choose typed SwiftUI for this two-pane copy task unless a concrete compatibility or integration requirement calls for providers or AppKit. The durable part is the contract: a defined value crosses the boundary, the destination validates it, and one model owner commits the accepted change.

Sources