Copy, Cut, and Paste in Text Editors
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.
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.
Family: Editor Features And Interaction
Use this skill when the main question is how paste, copy, or cut works in Apple text editors, or when you need to customize pasteboard behavior.
When to Use
Section titled “When to Use”- Sanitizing pasted rich text (stripping fonts, colors, or styles)
- Implementing custom pasteboard types for your editor
- Handling pasted images as
NSTextAttachmentobjects - Controlling what gets copied from your editor
- Bridging
NSItemProvidercontent into attributed strings
Quick Decision
Section titled “Quick Decision”- Need attachment rendering ->
/skill apple-text-attachments-ref - Need attributed string conversion ->
/skill apple-text-attributed-string - Need UIViewRepresentable bridging ->
/skill apple-text-representable
Core Guidance
Section titled “Core Guidance”Built-In Paste Behavior
Section titled “Built-In Paste Behavior”UITextView and NSTextView handle paste automatically. By default:
- Plain text paste: Inserts text with the text view’s
typingAttributes - Rich text paste: Inserts the attributed string preserving source formatting (fonts, colors, paragraph styles)
- Image paste: Creates an
NSTextAttachmentwith the image data
Controlling Paste via UITextViewDelegate
Section titled “Controlling Paste via UITextViewDelegate”func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { // This fires for typed text and paste // Return false to reject the edit return true}This delegate is limited — it only receives plain text, not the rich attributed string. For full paste control, override at the text view or responder level.
Stripping Formatting on Paste
Section titled “Stripping Formatting on Paste”UITextView: Override paste(_:)
Section titled “UITextView: Override paste(_:)”class PlainPasteTextView: UITextView { override func paste(_ sender: Any?) { // Read plain text from pasteboard, ignoring rich content guard let plainText = UIPasteboard.general.string else { return }
// Insert with current typing attributes let range = selectedRange textStorage.beginEditing() textStorage.replaceCharacters(in: range, with: plainText)
let insertedRange = NSRange(location: range.location, length: (plainText as NSString).length) textStorage.setAttributes(typingAttributes, range: insertedRange) textStorage.endEditing()
// Move cursor to end of insertion selectedRange = NSRange(location: insertedRange.location + insertedRange.length, length: 0) }}Selective Sanitization
Section titled “Selective Sanitization”Keep some attributes (bold, italic) but strip others (font name, colors):
func sanitizePastedAttributedString(_ source: NSAttributedString) -> NSAttributedString { let result = NSMutableAttributedString(string: source.string) let fullRange = NSRange(location: 0, length: result.length)
// Start with default attributes result.setAttributes(defaultAttributes, range: fullRange)
// Preserve only bold/italic from source source.enumerateAttributes(in: fullRange, options: []) { attrs, range, _ in if let font = attrs[.font] as? UIFont { let traits = font.fontDescriptor.symbolicTraits if traits.contains(.traitBold) { result.addAttribute(.font, value: boldFont, range: range) } if traits.contains(.traitItalic) { result.addAttribute(.font, value: italicFont, range: range) } } // Preserve links if let link = attrs[.link] { result.addAttribute(.link, value: link, range: range) } } return result}Handling Pasted Images
Section titled “Handling Pasted Images”Reading Images from Pasteboard
Section titled “Reading Images from Pasteboard”override func paste(_ sender: Any?) { let pasteboard = UIPasteboard.general
if pasteboard.hasImages, let image = pasteboard.image { insertImageAttachment(image) } else { super.paste(sender) // Default text paste }}
func insertImageAttachment(_ image: UIImage) { let attachment = NSTextAttachment() attachment.image = image
// Scale to fit text container width let maxWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2 if image.size.width > maxWidth { let scale = maxWidth / image.size.width attachment.bounds = CGRect(origin: .zero, size: CGSize(width: image.size.width * scale, height: image.size.height * scale)) }
let attrString = NSAttributedString(attachment: attachment) textStorage.insert(attrString, at: selectedRange.location)}NSItemProvider (Drag, Drop, and Modern Paste)
Section titled “NSItemProvider (Drag, Drop, and Modern Paste)”For iOS 16+ and modern drag-and-drop, content arrives via NSItemProvider:
func handleItemProviders(_ providers: [NSItemProvider]) { for provider in providers { if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in guard let data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { self.insertImageAttachment(image) } } } else if provider.hasItemConformingToTypeIdentifier(UTType.attributedString.identifier) { provider.loadObject(ofClass: NSAttributedString.self) { object, error in guard let attrString = object as? NSAttributedString else { return } DispatchQueue.main.async { let sanitized = self.sanitizePastedAttributedString(attrString) self.insertAttributedString(sanitized) } } } else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { provider.loadObject(ofClass: String.self) { object, error in guard let text = object as? String else { return } DispatchQueue.main.async { self.insertPlainText(text) } } } }}Custom Copy Behavior
Section titled “Custom Copy Behavior”Copying Rich Content
Section titled “Copying Rich Content”Override copy(_:) to write custom formats to the pasteboard:
override func copy(_ sender: Any?) { guard selectedRange.length > 0 else { return } let selectedAttrString = textStorage.attributedSubstring(from: selectedRange)
let pasteboard = UIPasteboard.general pasteboard.items = []
// Write multiple representations: rich, plain, and custom var items: [String: Any] = [:]
// Plain text items[UTType.plainText.identifier] = selectedAttrString.string
// Rich text (RTF) if let rtfData = try? selectedAttrString.data(from: NSRange(location: 0, length: selectedAttrString.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]) { items[UTType.rtf.identifier] = rtfData }
// Custom format (e.g., your app's internal representation) if let customData = encodeCustomFormat(selectedAttrString) { items["com.yourapp.richtext"] = customData }
pasteboard.addItems([items])}Reading Custom Formats on Paste
Section titled “Reading Custom Formats on Paste”override func paste(_ sender: Any?) { let pasteboard = UIPasteboard.general
// Prefer your custom format first if let customData = pasteboard.data(forPasteboardType: "com.yourapp.richtext") { let attrString = decodeCustomFormat(customData) insertAttributedString(attrString) } else if let rtfData = pasteboard.data(forPasteboardType: UTType.rtf.identifier) { let attrString = try? NSAttributedString(data: rtfData, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) if let attrString { insertAttributedString(sanitizePastedAttributedString(attrString)) } } else { super.paste(sender) }}Common Pitfalls
Section titled “Common Pitfalls”- Rich paste brings unwanted fonts. Source app fonts may not exist on the device. The system substitutes, but the result looks wrong. Always sanitize or remap fonts on paste.
- Pasted text loses typing attributes. When inserting plain text programmatically, apply
typingAttributesto the inserted range. The text view does this automatically for user paste but not for programmatic insertions. - NSItemProvider callbacks on background thread.
loadObjectandloadDataRepresentationcall back on arbitrary threads. Dispatch to main before touching text storage. shouldChangeTextIndoes not fire for programmatic paste. If you overridepaste(_:)and modify text storage directly, the delegate method is not called. Apply your own validation.- Undo after paste. Custom paste implementations must go through the normal editing lifecycle (
beginEditing/endEditing) to get proper undo registration. See/skill apple-text-undo.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-pasteboard workflow skill. Use it when the job is a guided review, implementation flow, or integration pass instead of a single API lookup.
Related
Section titled “Related”apple-text-attachments-ref: Use when embedding inline non-text content such as images, custom views, Genmoji, or attachment-backed runs inside Apple text systems. Reach for this when the problem is attachment APIs, layout, bounds, baseline alignment, or lifecycle, not broader rich-text architecture.apple-text-attributed-string: Use when choosing between AttributedString and NSAttributedString, defining custom attributes, converting between them, or deciding which model should own rich text in a feature. Reach for this when the main task is the attributed-string model decision, not low-level formatting catalog lookup.apple-text-undo: Use when implementing or debugging undo and redo in text editors, especially grouping, coalescing, programmatic edits, or integration with NSTextStorage, NSTextContentManager, or NSUndoManager. Reach for this when the problem is undo behavior, not generic editing lifecycle.
Full SKILL.md source
---name: apple-text-pasteboarddescription: 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.license: MIT---
# Copy, Cut, and Paste in Text Editors
Use this skill when the main question is how paste, copy, or cut works in Apple text editors, or when you need to customize pasteboard behavior.
## When to Use
- Sanitizing pasted rich text (stripping fonts, colors, or styles)- Implementing custom pasteboard types for your editor- Handling pasted images as `NSTextAttachment` objects- Controlling what gets copied from your editor- Bridging `NSItemProvider` content into attributed strings
## Quick Decision
- Need attachment rendering -> `/skill apple-text-attachments-ref`- Need attributed string conversion -> `/skill apple-text-attributed-string`- Need UIViewRepresentable bridging -> `/skill apple-text-representable`
## Core Guidance
## Built-In Paste Behavior
`UITextView` and `NSTextView` handle paste automatically. By default:
- **Plain text paste:** Inserts text with the text view's `typingAttributes`- **Rich text paste:** Inserts the attributed string preserving source formatting (fonts, colors, paragraph styles)- **Image paste:** Creates an `NSTextAttachment` with the image data
### Controlling Paste via UITextViewDelegate
```swiftfunc textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { // This fires for typed text and paste // Return false to reject the edit return true}```
This delegate is limited — it only receives plain text, not the rich attributed string. For full paste control, override at the text view or responder level.
## Stripping Formatting on Paste
### UITextView: Override paste(_:)
```swiftclass PlainPasteTextView: UITextView { override func paste(_ sender: Any?) { // Read plain text from pasteboard, ignoring rich content guard let plainText = UIPasteboard.general.string else { return }
// Insert with current typing attributes let range = selectedRange textStorage.beginEditing() textStorage.replaceCharacters(in: range, with: plainText)
let insertedRange = NSRange(location: range.location, length: (plainText as NSString).length) textStorage.setAttributes(typingAttributes, range: insertedRange) textStorage.endEditing()
// Move cursor to end of insertion selectedRange = NSRange(location: insertedRange.location + insertedRange.length, length: 0) }}```
### Selective Sanitization
Keep some attributes (bold, italic) but strip others (font name, colors):
```swiftfunc sanitizePastedAttributedString(_ source: NSAttributedString) -> NSAttributedString { let result = NSMutableAttributedString(string: source.string) let fullRange = NSRange(location: 0, length: result.length)
// Start with default attributes result.setAttributes(defaultAttributes, range: fullRange)
// Preserve only bold/italic from source source.enumerateAttributes(in: fullRange, options: []) { attrs, range, _ in if let font = attrs[.font] as? UIFont { let traits = font.fontDescriptor.symbolicTraits if traits.contains(.traitBold) { result.addAttribute(.font, value: boldFont, range: range) } if traits.contains(.traitItalic) { result.addAttribute(.font, value: italicFont, range: range) } } // Preserve links if let link = attrs[.link] { result.addAttribute(.link, value: link, range: range) } } return result}```
## Handling Pasted Images
### Reading Images from Pasteboard
```swiftoverride func paste(_ sender: Any?) { let pasteboard = UIPasteboard.general
if pasteboard.hasImages, let image = pasteboard.image { insertImageAttachment(image) } else { super.paste(sender) // Default text paste }}
func insertImageAttachment(_ image: UIImage) { let attachment = NSTextAttachment() attachment.image = image
// Scale to fit text container width let maxWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2 if image.size.width > maxWidth { let scale = maxWidth / image.size.width attachment.bounds = CGRect(origin: .zero, size: CGSize(width: image.size.width * scale, height: image.size.height * scale)) }
let attrString = NSAttributedString(attachment: attachment) textStorage.insert(attrString, at: selectedRange.location)}```
### NSItemProvider (Drag, Drop, and Modern Paste)
For iOS 16+ and modern drag-and-drop, content arrives via `NSItemProvider`:
```swiftfunc handleItemProviders(_ providers: [NSItemProvider]) { for provider in providers { if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in guard let data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { self.insertImageAttachment(image) } } } else if provider.hasItemConformingToTypeIdentifier(UTType.attributedString.identifier) { provider.loadObject(ofClass: NSAttributedString.self) { object, error in guard let attrString = object as? NSAttributedString else { return } DispatchQueue.main.async { let sanitized = self.sanitizePastedAttributedString(attrString) self.insertAttributedString(sanitized) } } } else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { provider.loadObject(ofClass: String.self) { object, error in guard let text = object as? String else { return } DispatchQueue.main.async { self.insertPlainText(text) } } } }}```
## Custom Copy Behavior
### Copying Rich Content
Override `copy(_:)` to write custom formats to the pasteboard:
```swiftoverride func copy(_ sender: Any?) { guard selectedRange.length > 0 else { return } let selectedAttrString = textStorage.attributedSubstring(from: selectedRange)
let pasteboard = UIPasteboard.general pasteboard.items = []
// Write multiple representations: rich, plain, and custom var items: [String: Any] = [:]
// Plain text items[UTType.plainText.identifier] = selectedAttrString.string
// Rich text (RTF) if let rtfData = try? selectedAttrString.data(from: NSRange(location: 0, length: selectedAttrString.length), documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]) { items[UTType.rtf.identifier] = rtfData }
// Custom format (e.g., your app's internal representation) if let customData = encodeCustomFormat(selectedAttrString) { items["com.yourapp.richtext"] = customData }
pasteboard.addItems([items])}```
### Reading Custom Formats on Paste
```swiftoverride func paste(_ sender: Any?) { let pasteboard = UIPasteboard.general
// Prefer your custom format first if let customData = pasteboard.data(forPasteboardType: "com.yourapp.richtext") { let attrString = decodeCustomFormat(customData) insertAttributedString(attrString) } else if let rtfData = pasteboard.data(forPasteboardType: UTType.rtf.identifier) { let attrString = try? NSAttributedString(data: rtfData, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) if let attrString { insertAttributedString(sanitizePastedAttributedString(attrString)) } } else { super.paste(sender) }}```
## Common Pitfalls
1. **Rich paste brings unwanted fonts.** Source app fonts may not exist on the device. The system substitutes, but the result looks wrong. Always sanitize or remap fonts on paste.2. **Pasted text loses typing attributes.** When inserting plain text programmatically, apply `typingAttributes` to the inserted range. The text view does this automatically for user paste but not for programmatic insertions.3. **NSItemProvider callbacks on background thread.** `loadObject` and `loadDataRepresentation` call back on arbitrary threads. Dispatch to main before touching text storage.4. **`shouldChangeTextIn` does not fire for programmatic paste.** If you override `paste(_:)` and modify text storage directly, the delegate method is not called. Apply your own validation.5. **Undo after paste.** Custom paste implementations must go through the normal editing lifecycle (`beginEditing`/`endEditing`) to get proper undo registration. See `/skill apple-text-undo`.
## Related Skills
- Use `/skill apple-text-attachments-ref` for attachment sizing, baseline, and view providers.- Use `/skill apple-text-attributed-string` for attribute conversion between paste formats.- Use `/skill apple-text-undo` for undo registration around paste operations.