Skip to content

Text Drag and Drop

Use when customizing drag and drop in Apple text editors, including UITextDraggable or UITextDroppable, drag previews, multi-line selections, iPhone drag enablement, or custom drop handling in UITextInput views. Reach for this when the task is editor drag-and-drop behavior, not pasteboard-only workflows.

Workflow Skills

Use when customizing drag and drop in Apple text editors, including UITextDraggable or UITextDroppable, drag previews, multi-line selections, iPhone drag enablement, or custom drop handling in UITextInput views. Reach for this when the task is editor drag-and-drop behavior, not pasteboard-only workflows.

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, and paste in Apple text editors, including stripping formatting, sanitizing rich text, custom pasteboard types, pasted attachments, or NSItemProvider bridging. Reach for this when the problem is pasteboard behavior, not general editor interaction.
  • apple-text-interaction: Use when customizing text-editor interactions in UIKit, such as selection behavior, edit menus, link taps, gestures, cursor appearance, or long-press actions. Reach for this when the problem is interaction behavior, not custom text input protocol plumbing.
  • apple-text-input-ref: Use when the user already knows the problem lives in the text input system and needs exact UITextInput, UIKeyInput, NSTextInputClient, marked-text, or selection-UI behavior. Reach for this when implementing or debugging custom text input plumbing, not high-level editor interactions alone.
Full SKILL.md source
SKILL.md
---
name: apple-text-drag-drop
description: Use when customizing drag and drop in Apple text editors, including UITextDraggable or UITextDroppable, drag previews, multi-line selections, iPhone drag enablement, or custom drop handling in UITextInput views. Reach for this when the task is editor drag-and-drop behavior, not pasteboard-only workflows.
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.