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
NSTextAttachmentobjects - Controlling what gets copied from your editor
- Bridging
NSItemProvidercontent 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
NSTextAttachmentwith 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
- 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.
Related Skills
- Use
/skill apple-text-attachments-reffor attachment sizing, baseline, and view providers. - Use
/skill apple-text-attributed-stringfor attribute conversion between paste formats. - Use
/skill apple-text-undofor undo registration around paste operations.