Skip to content

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.

Workflow Skills

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.

  • 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
  • Need attachment rendering -> /skill apple-text-attachments-ref
  • Need attributed string conversion -> /skill apple-text-attributed-string
  • Need UIViewRepresentable bridging -> /skill apple-text-representable

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

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

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

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

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.

  • 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
SKILL.md
---
name: apple-text-pasteboard
description: 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
```swift
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
### UITextView: Override paste(_:)
```swift
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
Keep some attributes (bold, italic) but strip others (font name, colors):
```swift
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
### Reading Images from Pasteboard
```swift
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)
For iOS 16+ and modern drag-and-drop, content arrives via `NSItemProvider`:
```swift
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
### Copying Rich Content
Override `copy(_:)` to write custom formats to the pasteboard:
```swift
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
```swift
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
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.