I have a custom NSTextView subclass that displays one or more NSTextAttachment objects that represent tokens (similar to NSTokenField).
The problem is that the contents property on NSTextAttachment is always nil after reading the NSAttributedString from the pasteboard, even though I see this data is being saved to the pasteboard.
- Create the text attachment and insert it into the text storage
let attachmentData = "\(key):\(value)".data(using: .utf8)
let attachment = NSTextAttachment(data: attachmentData, ofType: kUTTypeUTF8PlainText as String)
attachment.attachmentCell = CustomAttachmentCell(key: key, value: value)
...
textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment))
CustomAttachmentCellis a standardNSTextAttachmentCellsubclass that only handles custom drawing. It draws the text attachment within the text view as intended and uses standard CoreGraphics API calls to draw a background and text for the attachment's visual representation.
class CustomAttachmentCell: NSTextAttachmentCell {
let key: String
let value: String
override func draw(withFrame cellFrame: NSRect, in controlView: NSView?) {
drawKey(withFrame: cellFrame, in: controlView)
drawValue(withFrame: cellFrame, in: controlView)
}
}
- Configure
NSTextViewDelegatemethods to handle writing the attachment to the pasteboard
// If the previous method is not used, this method and the next allow the textview to take care of attachment dragging and pasting, with the delegate responsible only for writing the attachment to the pasteboard.
// In this method, the delegate should return an array of types that it can write to the pasteboard for the given attachment.
func textView(_ view: NSTextView, writablePasteboardTypesFor cell: NSTextAttachmentCellProtocol, at charIndex: Int) -> [NSPasteboard.PasteboardType] {
return [(kUTTypeFlatRTFD as NSPasteboard.PasteboardType)]
}
// In this method, the delegate should attempt to write the given attachment to the pasteboard with the given type, and return success or failure.
func textView(_ view: NSTextView, write cell: NSTextAttachmentCellProtocol, at charIndex: Int, to pboard: NSPasteboard, type: NSPasteboard.PasteboardType) -> Bool {
guard type == kUTTypeFlatRTFD as NSPasteboard.PasteboardType else { return false }
guard let attachment = cell.attachment else { return false }
return pboard.writeObjects([NSAttributedString(attachment: attachment)])
}
- After right-clicking the text attachment in the text view and choosing "cut" from the context menu, I can see the RTFD data in the pasteboard using the Clipboard Viewer.app:
rtfd............
...Attachment.........TXT.rtf....M...÷...........exampleKey:exampleValue....E.......
...Attachment....TXT.rtf..........Ñ`¶...........².Ñ`¶...............ï...{\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf600
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\f0\fs24 \cf0 {{\NeXTGraphic Attachment \width640 \height640 \appleattachmentpadding0 \appleembedtype0 \appleaqc
}¬}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\cf0 \
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
\cf0 {{\NeXTGraphic Attachment \width640 \height640 \appleattachmentpadding0 \appleembedtype0 \appleaqc
}¬}}
- Now when I paste back into the text view and
textStorageWillProcessEditing()is called. I'm expecting to be able to retrieve the text attachment contents property at this point, but it is always nil. Am I missing some critical piece in this chain?
override func textStorageWillProcessEditing(_ notification: Notification) {
guard let attributedString = notification.object as? NSAttributedString else { return }
attributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedString.length), options: [], using: { (value, range, stop) in
if let attachment = value as? NSTextAttachment {
// attachment.contents is nil here, I was expecting a Data object representing the UTF-8 string "exampleKey:exampleValue"
}
})
}
Alternatively I've tried overriding some of the pasteboard-related methods in my NSTextView subclass to do the same thing (read/write rftd data).
- Overrode
readablePasteboardTypes()to reorder so that the richest data (e.g. com.apple.flat-rtfd / NeXT RTFD pasteboard type) are first in the array. - Overrode
preferredPasteboardType(from: restrictedToTypesFrom:)to return(kUTTypeFlatRTFD as NSPasteboard.PasteboardType) - Overrode
readSelection(from:type:)
When overriding the methods above I see the same behavior where I can:
- Read
NSAttributedStringfrom the pasteboard - Enumerate the
NSTextAttachmentobjects contained within the attributed string - The enumerated
NSTextAttachmentobjectscontentsproperty is always set to nil instead of theDatarepresenting the UTF-8 string "exampleKey:exampleValue" that was written to the pasteboard inside theNSTextAttachmentwhich is inside theNSAttributedString.
It appears that this is quite an esoteric problem as all the sample code and search results I've found all either use NSFileWrapper or NSImage/UIImage to back their NSTextAttachment rather than the NSTextAttachment(data:ofType:) initializer.
Thanks!