apple-text-pasteboard

Use when handling copy, cut, or paste in text editors — format stripping, rich text sanitization, custom pasteboard types

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

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(_:)

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

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

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:

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:

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

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.