apple-text-drag-drop

Use when customizing drag and drop in Apple text editors — UITextDraggable, UITextDroppable, drag previews, or custom drop handling

Text Drag and Drop

Use this skill when the main question is how text-specific drag and drop works in Apple text editors, or when customizing drag/drop behavior beyond the defaults.

When to Use

  • Customizing what gets dragged from a text view
  • Controlling drop behavior (insert, replace selection, replace all)
  • Enabling text drag on iPhone (disabled by default)
  • Building drag/drop for a custom UITextInput view
  • Handling drops in non-editable text views
  • Custom drag previews for multi-line text

Quick Decision

Using UITextView or UITextField?
    → Drag and drop works automatically (iPad). Configure via delegates.
    → iPhone: must enable explicitly.

Building a custom UITextInput view?
    → Text drag/drop protocols are NOT automatically adopted.
    → Must add UIDragInteraction / UIDropInteraction manually.

Need copy/paste instead of drag/drop?
    → /skill apple-text-pasteboard

macOS?
    → Different architecture (NSDraggingSource / NSDraggingDestination)

Core Guidance

Default Behavior

UITextView and UITextField conform to UITextDraggable and UITextDroppable automatically.

Drag: User selects text, long-presses the selection to lift it. The system creates drag items with the selected text.

Drop: Text views accept dropped text, inserting at the position under the user's finger. The caret tracks the drag position.

Move vs copy:

  • Same text view → move (dragged text removed from original position)
  • Different view or app → copy

iPhone vs iPad

// iPad: drag enabled by default
// iPhone: drag DISABLED by default
textView.textDragInteraction?.isEnabled = true  // Enable on iPhone

UITextDragDelegate

All methods are optional. Set via textView.textDragDelegate = self.

Providing Custom Drag Items

func textDraggableView(_ textDraggableView: UIView & UITextDraggable,
                       itemsForDrag dragRequest: UITextDragRequest) -> [UIDragItem] {
    // Return custom items (e.g., add image alongside text)
    let text = dragRequest.suggestedItems.first?.localObject as? String ?? ""
    let textItem = UIDragItem(itemProvider: NSItemProvider(object: text as NSString))

    // Add a custom representation
    let customData = encodeRichFormat(for: dragRequest.dragRange)
    let customItem = UIDragItem(itemProvider: NSItemProvider(
        item: customData as NSData,
        typeIdentifier: "com.myapp.richtext"
    ))

    return [textItem, customItem]
}

Disabling Drag

// Option A: Return empty array
func textDraggableView(_ textDraggableView: UIView & UITextDraggable,
                       itemsForDrag dragRequest: UITextDragRequest) -> [UIDragItem] {
    return []  // Disables drag
}

// Option B: Disable the interaction
textView.textDragInteraction?.isEnabled = false

Custom Drag Preview

func textDraggableView(_ textDraggableView: UIView & UITextDraggable,
                       dragPreviewForLiftingItem item: UIDragItem,
                       session: UIDragSession) -> UITargetedDragPreview? {
    // Return nil for default preview
    // Return custom UITargetedDragPreview for custom appearance
    return nil
}

UITextDragPreviewRenderer

Text-aware preview rendering that understands multi-line text geometry:

// Create a renderer from layout manager and range
let renderer = UITextDragPreviewRenderer(
    layoutManager: textView.layoutManager,
    range: selectedRange
)

// Adjust the preview rectangles
renderer.adjust(firstLineRect: &firstRect,
                bodyRect: &bodyRect,
                lastLineRect: &lastRect,
                textOrigin: origin)

// The renderer provides proper multi-line drag previews
// that follow the text's line geometry

Strip Color from Previews

textView.textDragOptions = .stripTextColorFromPreviews
// Renders drag preview in uniform color instead of preserving text colors

Lifecycle Hooks

func textDraggableView(_ textDraggableView: UIView & UITextDraggable,
                       dragSessionWillBegin session: UIDragSession) {
    // Drag is starting — pause syncing, show visual feedback
}

func textDraggableView(_ textDraggableView: UIView & UITextDraggable,
                       dragSessionDidEnd session: UIDragSession) {
    // Drag ended — resume normal operations
}

UITextDropDelegate

All methods are optional. Set via textView.textDropDelegate = self.

Controlling Drop Behavior

func textDroppableView(_ textDroppableView: UIView & UITextDroppable,
                       proposalForDrop drop: UITextDropRequest) -> UITextDropProposal {
    // Check if this is a same-view drop (move vs copy)
    if drop.isSameView {
        return UITextDropProposal(dropAction: .insert)
    }

    // Accept external drops as insert at drop position
    return UITextDropProposal(dropAction: .insert)
}

UITextDropProposal Actions

ActionBehavior
.insertInsert at drop position (default)
.replaceSelectionReplace the current text selection
.replaceAllReplace all text in the view
// Replace selection on drop
let proposal = UITextDropProposal(dropAction: .replaceSelection)

// Optimize same-view operations
proposal.useFastSameViewOperations = true

Handling the Drop

func textDroppableView(_ textDroppableView: UIView & UITextDroppable,
                       willPerformDrop drop: UITextDropRequest) {
    // Called just before the drop executes
    // Use for validation, logging, or pre-processing
}

Non-Editable Text Views

By default, non-editable text views reject drops. Override with:

func textDroppableView(_ textDroppableView: UIView & UITextDroppable,
                       willBecomeEditableForDrop drop: UITextDropRequest) -> UITextDropEditability {
    return .temporary  // Become editable just for this drop, then revert
    // .no — reject the drop (default for non-editable)
    // .yes — become permanently editable
}

Custom Drop Preview

func textDroppableView(_ textDroppableView: UIView & UITextDroppable,
                       previewForDroppingAllItemsWithDefault defaultPreview: UITargetedDragPreview) -> UITargetedDragPreview? {
    // Return nil for default animation
    // Return custom preview for custom drop animation
    return nil
}

macOS (AppKit)

macOS text drag/drop uses a completely different architecture — no UITextDragDelegate equivalent.

NSTextView Default Behavior

NSTextView uses NSDraggingSource and NSDraggingDestination (which NSView conforms to). By default:

  • Text can be dragged from selections
  • Text drops are accepted if the view is editable
  • File drops are only accepted if isRichText and importsGraphics are both enabled

Customizing on macOS

class CustomTextView: NSTextView {
    // Accept additional pasteboard types
    override var acceptableDragTypes: [NSPasteboard.PasteboardType] {
        var types = super.acceptableDragTypes
        types.append(.init("com.myapp.richtext"))
        return types
    }

    // Handle the drop
    override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
        let pasteboard = draggingInfo.draggingPasteboard
        if let customData = pasteboard.data(forType: .init("com.myapp.richtext")) {
            insertCustomContent(customData)
            return true
        }
        return super.performDragOperation(draggingInfo)
    }
}

Field Editor Gotcha

During NSTextField editing, drops go to the field editor (shared NSTextView), not the NSTextField itself. To intercept:

func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
    if client is MyTextField {
        return myCustomFieldEditor  // Subclass NSTextView with custom drop handling
    }
    return nil
}

Custom UITextInput Views

UITextDraggable and UITextDroppable are NOT automatically adopted by custom UITextInput views. Only UITextField and UITextView get them.

For custom views, use the general drag/drop APIs:

class CustomEditor: UIView, UITextInput {
    override init(frame: CGRect) {
        super.init(frame: frame)

        // Add general drag/drop interactions
        let dragInteraction = UIDragInteraction(delegate: self)
        addInteraction(dragInteraction)

        let dropInteraction = UIDropInteraction(delegate: self)
        addInteraction(dropInteraction)
    }
}

extension CustomEditor: UIDragInteractionDelegate {
    func dragInteraction(_ interaction: UIDragInteraction,
                        itemsForBeginning session: any UIDragSession) -> [UIDragItem] {
        guard let selectedText = textInSelectedRange() else { return [] }
        let provider = NSItemProvider(object: selectedText as NSString)
        return [UIDragItem(itemProvider: provider)]
    }
}

extension CustomEditor: UIDropInteractionDelegate {
    func dropInteraction(_ interaction: UIDropInteraction,
                        performDrop session: any UIDropSession) {
        // Handle the drop using UITextInput methods
        // Convert drop point to UITextPosition
        let point = session.location(in: self)
        guard let position = closestPosition(to: point) else { return }
        // Insert text at position
    }
}

Platform Comparison

FeatureiOS (UITextView)macOS (NSTextView)
Text drag/drop protocolsUITextDraggable / UITextDroppableNSDraggingSource / NSDraggingDestination
Specialized delegatesUITextDragDelegate / UITextDropDelegateNone (general dragging APIs)
Drop proposal systemUITextDropProposal with actionsperformDragOperation override
Multi-line previewUITextDragPreviewRendererSystem-provided
iPhone dragDisabled by defaultN/A
File drops on textSupportedOnly if isRichText + importsGraphics
Move vs copyAutomatic (same view = move)Manual via operation mask

Common Pitfalls

  1. Drag not working on iPhonetextDragInteraction?.isEnabled defaults to false on iPhone. Must enable explicitly.
  2. Non-editable views rejecting drops — Implement willBecomeEditableForDrop returning .temporary to accept drops on read-only views.
  3. Custom UITextInput views have no text drag/drop — Must add UIDragInteraction/UIDropInteraction manually. The text-specific protocols only apply to UITextField and UITextView.
  4. Attributed text ignored in drag previews — Known issue (rdar://34098227). Provide custom drag items to ensure attributed text is included.
  5. macOS field editor intercepts drops — Drops during NSTextField editing go to the field editor, not the text field. Provide a custom field editor to intercept.
  6. Move vs copy confusion — Same-view drops default to move (source text deleted). Cross-view defaults to copy. Check drop.isSameView in your proposal.
  7. NSTextView not accepting file drops — Both isRichText and importsGraphics must be enabled for file drop acceptance on macOS.

Related Skills

  • Use /skill apple-text-pasteboard for copy/paste (synchronous clipboard operations).
  • Use /skill apple-text-interaction for other gesture and interaction customization.
  • Use /skill apple-text-input-ref for the UITextInput protocol that custom drag/drop builds on.
  • Use /skill apple-text-attachments-ref when dropped content becomes inline attachments.