Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Paste Formatted Text, Not Images or HTML

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

  1. If you copy formatted text from the Notes app, Evernote, or text and images from a web site, Pages will only paste the plain text string
  2. If you copy formatted text from within Pages or Keynote, it will paste the formatted text elsewhere in Pages, Keynote, etc.
  3. An undesired consequence, but perhaps important to acknowledge, is that neither Notes app or Evernote will paste formatted text copied from Pages or Keynote. I am speculating that the discrepancy between apps is the use of NSAttributedStrings, versus HTML?

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.

enter image description here

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.

like image 627
DenVog Avatar asked Jan 04 '15 19:01

DenVog


People also ask

How do I paste without formatting in HTML?

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.


2 Answers

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"
  • Override UITextView copy: and use your custom pasteboard 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.

  • (Bonus: help UX in other apps by stripping away unnecessary attributes you've added, on copy (like font and fg-color))
  • 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

like image 189
Leonard Pauli Avatar answered Sep 22 '22 20:09

Leonard Pauli


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.

like image 23
Angela Avatar answered Sep 26 '22 20:09

Angela