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
| Action | Behavior |
|---|---|
.insert | Insert at drop position (default) |
.replaceSelection | Replace the current text selection |
.replaceAll | Replace 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
isRichTextandimportsGraphicsare 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
| 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
- Drag not working on iPhone —
textDragInteraction?.isEnableddefaults tofalseon iPhone. Must enable explicitly. - Non-editable views rejecting drops — Implement
willBecomeEditableForDropreturning.temporaryto accept drops on read-only views. - Custom UITextInput views have no text drag/drop — Must add UIDragInteraction/UIDropInteraction manually. The text-specific protocols only apply to UITextField and UITextView.
- Attributed text ignored in drag previews — Known issue (rdar://34098227). Provide custom drag items to ensure attributed text is included.
- 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.
- Move vs copy confusion — Same-view drops default to move (source text deleted). Cross-view defaults to copy. Check
drop.isSameViewin your proposal. - NSTextView not accepting file drops — Both
isRichTextandimportsGraphicsmust be enabled for file drop acceptance on macOS.
Related Skills
- Use
/skill apple-text-pasteboardfor copy/paste (synchronous clipboard operations). - Use
/skill apple-text-interactionfor other gesture and interaction customization. - Use
/skill apple-text-input-reffor the UITextInput protocol that custom drag/drop builds on. - Use
/skill apple-text-attachments-refwhen dropped content becomes inline attachments.