I am trying to emulate the pasteboard behavior of the iOS Pages and Keynote apps. In short, allowing basic NSAttributedString text formatting (i.e. BIU) to be pasted into a UITextView, but not images, HTML, etc.
BEHAVIOR SUMMARY
How is this accomplished? On Mac OS, it appears you can ask the pasteboard to return different types of itself, have it provide both a rich text and a string representation, and use rich text as preferred. Unfortunately, the readObjectsForClasses doesn't appear to exist for iOS. That said, I can see via log that iOS does have an RTF related type of pasteboard, thanks to this post. I can't however, find a way to request an NSAttributedString version of pasteboard contents so I can prioritize it for pasting.
BACKGROUND
I have an app that allows basic NSAttributedString user editable formatting (i.e. bold, italic, underline) of text in UITextViews. Users want to copy text from other apps (e.g. web page in Safari, text in Notes app), to paste into a UITextView in my app. Allowing pasteboard to operate as default means I may end up with background colors, images, fonts, etc. that my app isn't intended to handle. Example below shows text how copied text with a background color looks when pasted into my app's UITextView.
I can overcome 1 by subclassing UITextView
- (void)paste:(id)sender
{
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
NSString *string = pasteBoard.string;
NSLog(@"Pasteboard string: %@", string);
[self insertText:string];
}
The unintended consequence is, losing the ability to retain formatting of text that's copied from within my app. Users may want to copy text from one UITextView in my app, and paste it to another UITextView in my app. They will expect formatting (i.e. bold, italics, underline) to be retained.
Insight and suggestions appreciated.
Paste Without Formatting Using Keyboard Shortcuts On Windows, while it's not universal, many apps support the shortcut Ctrl + Shift + V to paste without formatting. These include Chrome, Firefox, and Evernote.
Some copy/paste goodies and ideas, for you :)
// Setup code in overridden UITextView.copy/paste
let pb = UIPasteboard.generalPasteboard()
let selectedRange = self.selectedRange
let selectedText = self.attributedText.attributedSubstringFromRange(selectedRange)
// UTI List
let utf8StringType = "public.utf8-plain-text"
let rtfdStringType = "com.apple.flat-rtfd"
let myType = "com.my-domain.my-type"
pb.setValue(selectedText.string, forPasteboardType: myType)
To allow rich text copy (in copy:):
// Try custom copy
do {
// Convert attributedString to rtfd data
let fullRange = NSRange(location: 0, length: selectedText.string.characters.count)
let data:NSData? = try selectedText.dataFromRange(fullRange, documentAttributes: [NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType])
if let data = data {
// Set pasteboard values (rtfd and plain text fallback)
pb.items = [[rtfdStringType: data], [utf8StringType: selectedText.string]]
return
}
} catch { print("Couldn't copy") }
// If custom copy not available;
// Copy as usual
super.copy(sender)
To allow rich text paste (in paste:):
// Custom handling for rtfd type pasteboard data
if let data = pb.dataForPasteboardType(rtfdStringType) {
do {
// Convert rtfd data to attributedString
let attStr = try NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType], documentAttributes: nil)
// Bonus: Possibly strip all unwanted attributes here.
// Insert it into textview
replaceSelection(attStr)
return
} catch {print("Couldn't convert pasted rtfd")}
}
// Default handling otherwise (plain-text)
else { super.paste(sender) }
Even better then using a custom pasteboard type, white-list all possibly wanted tags, loop through and strip away all other on paste.
Also worth noting, the textView might not want to allow pasting when the pasteboard contains a specific type, to fix that:
// Allow all sort of paste (might want to use a white list to check pb.items agains here)
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UITextView.paste(_:)) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
Furthermore, cut: could be nice to implement as well. Basically just copy:
and replaceSelection(emptyString)
For your convenience:
// Helper to insert attributed text at current selection/cursor position
func replaceSelection(attributedString: NSAttributedString) {
var selectedRange = self.selectedRange
let m = NSMutableAttributedString(attributedString: self.attributedText)
m.replaceCharactersInRange(self.selectedRange, withAttributedString: attributedString)
selectedRange.location += attributedString.string.characters.count
selectedRange.length = 0
self.attributedText = m
self.selectedRange = selectedRange
}
Good luck!
Refs: Uniform Type Identifiers Reference
This should be a comment on Leonard Pauli's answer, but I don't have enough reputation to make comments yet.
Instead of:
selectedRange.location += attributedString.string.characters.count
(or attributedString.string.count as it is in more recent versions of Swift)
It's best to use:
selectedRange.location += attributedString.length
Otherwise when you paste text that contains emoji that cause attributedString.length and attributedString.string.count to differ, the selection will end up in the wrong place.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With