To make a synthetic card appear intact during a native macOS drag, give its outer container one drag source and render the complete card as its preview. Use AppKit when you need explicit control of the preview’s initial frame and pointer offset.
This guide covers dragging one in-memory card between two panels in your own Mac app. The practical goal is to preserve the card’s shapes, text, spacing, and identity while keeping gesture handling and model updates under clear ownership.
Which part of the drag needs to change?
Apple separates the transferred data from the visual representation shown during dragging. SwiftUI’s draggable(_:preview:) generates a rendering of your preview view when the drag begins. The documented preview is centered over the source view. It does not transfer the source view itself. See Apple’s draggable(_:preview:) documentation.
Use this decision aid to identify the control you actually need:
| Desired behavior | Supported starting point |
|---|---|
| Keep the card’s label, shapes, and background together | One outer drag source with one complete custom preview |
| Adjust the outline during the lift transition | contentShape(.dragPreview, shape) |
| Change which part of the card can initiate dragging | contentShape(.interaction, shape) |
| Commit a card to another panel | Validate the payload and update the model in the destination |
| Set the initial drag image rectangle and pointer relationship explicitly | An AppKit-owned session using NSDraggingItem |
| Move an element continuously within a canvas without native drop destinations | A local DragGesture interaction |
The distinction between the two content shapes matters: Apple documents that .dragPreview affects the preview shape, while .interaction controls hit testing. Changing the preview outline alone does not enlarge the region that starts the drag. See ContentShapeKinds.dragPreview.
A local DragGesture reports a dragging motion through a gesture sequence. Choose that approach when the task is updating a canvas position. For this article’s panel-to-panel native drop task, let draggable install the drag gestures. See Apple’s DragGesture reference and drag-source guide.
Build one preview for the entire synthetic card
Place the drag modifier on the smallest container that includes everything that should travel together. For a card made from a circle, two text lines, and a rounded background, that container is the complete card. Attaching separate drag sources to its children would describe separate interactions.
Reuse one presentation view for the source and preview. Give that view explicit dimensions and its own background, rather than depending on styling inherited from a surrounding panel. This is a design recommendation derived from the rendered-preview behavior: a self-contained presentation makes matching the two appearances easier.
Complete SwiftUI example
Create a macOS SwiftUI app with a deployment target of macOS 13 or later. Replace the generated app entry and content view with this single file, leaving exactly one @main declaration in the target. Both the custom-preview modifier and the destination overload used here are documented as available from macOS 13. See draggable(_:preview:) and dropDestination(for:action:isTargeted:).
import Foundation
import SwiftUI
@main
struct IntactDragDemoApp: App {
var body: some Scene {
WindowGroup {
BoardView()
}
}
}
private struct CardFace: View {
var body: some View {
HStack(spacing: 14) {
Circle()
.fill(Color.orange)
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 6) {
Text("Tile A")
.font(.headline)
Text("Circle and label")
.font(.caption)
}
.foregroundStyle(Color.white)
}
.padding(16)
.frame(width: 220, height: 100, alignment: .leading)
.background(Color.indigo)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
private struct BoardView: View {
@State private var token = UUID().uuidString
@State private var isOnRight = false
var body: some View {
VStack(spacing: 20) {
Text("Drag the card between panels")
.font(.headline)
HStack(spacing: 24) {
panel(right: false)
panel(right: true)
}
}
.padding(24)
}
private func panel(right: Bool) -> some View {
VStack(spacing: 16) {
Text(right ? "Right" : "Left")
.font(.headline)
ZStack {
if isOnRight == right {
CardFace()
.contentShape(
.interaction,
RoundedRectangle(cornerRadius: 16)
)
.contentShape(
.dragPreview,
RoundedRectangle(cornerRadius: 16)
)
.draggable(token) {
CardFace()
}
} else {
Text("Drop here")
.foregroundStyle(.secondary)
}
}
.frame(width: 240, height: 120)
}
.frame(width: 260, height: 180)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.gray.opacity(0.12))
)
.contentShape(Rectangle())
.dropDestination(for: String.self) { items, _ in
guard items == [token] else {
return false
}
isOnRight = right
return true
}
}
}
This example uses only synthetic graphics and a generated in-memory identifier. A String already conforms to Transferable, so it needs no custom transfer representation. Apple documents that built-in conformance in Making a view into a drag source.
The expected interaction is straightforward: drag Tile A into the other panel, release it, then drag it back. The complete CardFace supplies the moving preview. The destination accepts exactly one matching token and changes the single location value. Returning true reports a successful drop; returning false rejects it, following the destination action’s documented contract.
This is a complete example intended for a macOS app target. It has not been compiled or run here because the research environment has no Swift toolchain or macOS runtime.
Keep visual continuity separate from model ownership
In the example, the original card stays visible until an accepted drop changes isOnRight. Releasing outside the panels makes no model change. Dropping back into the occupied panel assigns the existing location again. A mismatched token or multiple items are rejected before state changes.
That behavior avoids removing the card at drag initiation and then needing to reconstruct it after cancellation. The preview provides visual continuity; the destination owns the location change. For a larger board, apply the same pattern to a stable item identifier and one authoritative collection.
Swift value semantics help explain why passing a payload does not move the original model automatically. Structures and strings have value semantics; assigning or passing them does not implicitly relocate an item in your application’s collection. Your model update must express that relocation. See Structures and Classes.
Also distinguish an application-level relocation from the native drag operation. The example changes one shared board state after an accepted drop; it does not configure native move negotiation or guarantee a move cursor.
For macOS 26 and later, Apple documents DragConfiguration for declaring supported operations and onDragSessionUpdated(_:) for observing the performed operation. A destination chooses the operation it performs. If you implement source removal after a native move, coordinate it with destination handling so the same shared item is not removed twice. See DragConfiguration and onDragSessionUpdated(_:).
When should AppKit own the drag session?
Use AppKit when the acceptance criterion includes an explicitly positioned drag image, such as keeping the pointer over the same point inside the card at pickup. SwiftUI’s documented custom-preview modifier takes a payload and a preview view, and describes a centered preview. AppKit exposes the dragging item’s frame directly.
For an AppKit implementation, follow this sequence:
- Let the source
NSViewreceive the actual mouse-down event and track whether the interaction becomes a drag. - Render the complete synthetic card into one
NSImageand create oneNSDraggingItemwith its pasteboard writer. - Set the image and its initial rectangle with
setDraggingFrame(_:contents:). - Express that rectangle in the initiating view’s coordinate system, then call
beginDraggingSession(with:event:source:)with the initiating mouse-down event and yourNSDraggingSource. - Let the destination validate and commit the drop. Use the source’s completion callback for session cleanup.
Apple specifies that the initiating event’s mouse location determines the dragged icon’s offset, and that each item’s initial frame must use the source view’s coordinates. Keep the card rectangle and event location in a consistent coordinate system. See beginDraggingSession(with:event:source:).
setDraggingFrame(_:contents:) is the documented convenience method for a dragging item represented by one image. A synthetic card can contain several drawn elements while still being represented by that single image. See setDraggingFrame(_:contents:).
Preserve event and session boundaries
Choose one drag initiator for the card’s interactive region. As an implementation rule, avoid layering an AppKit session starter over an active SwiftUI draggable source for that same region. Both would attempt to own the interaction. Do not manufacture mouse events to compensate for unclear gesture ownership.
You can embed the AppKit source using NSViewRepresentable. Keep native event handling in its NSView, and communicate accepted model changes through bindings or a coordinator. SwiftUI controls the represented view’s layout; Apple specifically says not to set that managed view’s frame or bounds directly. A dragging item’s image rectangle is a separate concern. See NSViewRepresentable.
Do not retain an NSDraggingItem and mutate it later. Apple documents its limited lifetime: after session creation, change dragging items through session enumeration, and use each enumerated item only inside its callback. Destination-side changes can also override source-side image changes while the drag is over that destination. See NSDraggingItem and enumerateDraggingItems(options:for:classes:searchOptions:using:).
How do you diagnose a preview that still looks wrong?
Check the failure at the stage where it appears:
| Symptom | First check |
|---|---|
| Only the circle or text travels | Put one drag source on the complete card container. |
| Preview dimensions differ from the card | Reuse the same presentation view and explicit dimensions. |
| Pickup outline differs from the moving image | Check .dragPreview separately from the custom preview closure. |
| Empty space unexpectedly starts the drag | Review .interaction and the source container’s bounds. |
| AppKit image jumps at pickup | Check the initiating mouse-down event and source-view coordinates. |
| Later AppKit image changes do nothing | Perform them inside session enumeration. |
| Card disappears after cancellation | Move model mutation to accepted-drop handling and review cleanup. |
Verify cancellation, an accepted drop, a drop back onto the original panel, and a rejected payload separately. The useful invariant is that one model location remains authoritative throughout. Start with the complete SwiftUI preview, then adopt an AppKit-owned source only when the required image placement needs its explicit frame and event controls.
Sources
draggable(_:preview:) — Apple Developer Documentation.
Making a view into a drag source — Apple Developer Documentation.
dragPreview — Apple Developer Documentation.
DragGesture — Apple Developer Documentation.
dropDestination(for:action:isTargeted:) — Apple Developer Documentation.
Structures and Classes — Swift.org.
DragConfiguration — Apple Developer Documentation.
onDragSessionUpdated(_:) — Apple Developer Documentation.
beginDraggingSession(with:event:source:) — Apple Developer Documentation.
setDraggingFrame(_:contents:) — Apple Developer Documentation.
NSViewRepresentable — Apple Developer Documentation.
NSDraggingItem — Apple Developer Documentation.
enumerateDraggingItems(options:for:classes:searchOptions:using:) — Apple Developer Documentation.