Skip to content

Text Drag and Drop

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

Workflow Skills

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

Family: Editor Features And Interaction

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.

  • 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
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)

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
// iPad: drag enabled by default
// iPhone: drag DISABLED by default
textView.textDragInteraction?.isEnabled = true // Enable on iPhone

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

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

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
textView.textDragOptions = .stripTextColorFromPreviews
// Renders drag preview in uniform color instead of preserving text colors
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
}

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

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

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

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

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
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)
}
}

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
}

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
}
}
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
  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.

This page documents the apple-text-drag-drop workflow skill. Use it when the job is a guided review, implementation flow, or integration pass instead of a single API lookup.

  • apple-text-pasteboard: Use when handling copy, cut, or paste in text editors — format stripping, rich text sanitization, custom pasteboard types.
  • apple-text-interaction: Use when customizing selection, edit menus, link taps, gestures, cursor appearance, or long-press actions in text editors.
  • apple-text-input-ref: Use when implementing or debugging UITextInput, UIKeyInput, or NSTextInputClient — marked text, selection UI, custom input.
Full SKILL.md source
SKILL.md
---
name: apple-text-drag-drop
description: Use when customizing drag and drop in Apple text editors — UITextDraggable, UITextDroppable, drag previews, or custom drop handling
license: MIT
---
# 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
```swift
// 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
```swift
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
```swift
// 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
```swift
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:
```swift
// 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
```swift
textView.textDragOptions = .stripTextColorFromPreviews
// Renders drag preview in uniform color instead of preserving text colors
```
### Lifecycle Hooks
```swift
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
```swift
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
| Action | Behavior |
|--------|----------|
| `.insert` | Insert at drop position (default) |
| `.replaceSelection` | Replace the current text selection |
| `.replaceAll` | Replace all text in the view |
```swift
// Replace selection on drop
let proposal = UITextDropProposal(dropAction: .replaceSelection)
// Optimize same-view operations
proposal.useFastSameViewOperations = true
```
### Handling the Drop
```swift
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:
```swift
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
```swift
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
```swift
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:
```swift
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:
```swift
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
| Feature | iOS (UITextView) | macOS (NSTextView) |
|---------|------------------|-------------------|
| Text drag/drop protocols | UITextDraggable / UITextDroppable | NSDraggingSource / NSDraggingDestination |
| Specialized delegates | UITextDragDelegate / UITextDropDelegate | None (general dragging APIs) |
| Drop proposal system | UITextDropProposal with actions | performDragOperation override |
| Multi-line preview | UITextDragPreviewRenderer | System-provided |
| iPhone drag | Disabled by default | N/A |
| File drops on text | Supported | Only if isRichText + importsGraphics |
| Move vs copy | Automatic (same view = move) | Manual via operation mask |
## Common Pitfalls
1. **Drag not working on iPhone**`textDragInteraction?.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.