12

How do you copy an NSAttributedString in the pasteboard, to allow the user to paste, or to paste programmatically (with - (void)paste:(id)sender, from UIResponderStandardEditActions protocol).

I tried:

UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
[pasteBoard setValue:attributedString forPasteboardType:(NSString *)kUTTypeRTF];

but this crash with:

-[UIPasteboard setValue:forPasteboardType:]: value is not a valid property list type'

which is to be expected, because NSAttributedString is not a property list value.

If the user paste the content of the pasteboard in my app, I would like to keep all the standards and custom attributes of the attributed string.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
Guillaume
  • 21,685
  • 6
  • 63
  • 95
  • Made some insights in UIPasteBoard and NSAttributedString, could be valuable: http://stackoverflow.com/a/38211885/1054573 – Leonard Pauli Jul 05 '16 at 20:20

5 Answers5

11

Instead of involving HTML, the clean solution is to insert NSAttributedString as RTF (plus plaintext fallback) into the paste board:

- (void)setAttributedString:(NSAttributedString *)attributedString {
    NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
                               documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType}
                                            error:nil];
    self.items = @[@{(id)kUTTypeRTF: [[NSString alloc] initWithData:rtf encoding:NSUTF8StringEncoding],
                     (id)kUTTypeUTF8PlainText: attributedString.string}];
}

Swift 5

import MobileCoreServices

public extension UIPasteboard {
    func set(attributedString: NSAttributedString) {
        do {
            let rtf = try attributedString.data(from: NSMakeRange(0, attributedString.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf])
            items = [[kUTTypeRTF as String: NSString(data: rtf, encoding: String.Encoding.utf8.rawValue)!, kUTTypeUTF8PlainText as String: attributedString.string]]
        } catch {
        }
    }
}
Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
10

I have found that when I (as a user of the application) copy rich text from a UITextView into the pasteboard, the pasteboard contains two types:

"public.text",
"Apple Web Archive pasteboard type

Based on that, I created a convenient category on UIPasteboard.
(With heavy use of code from this answer).

It works, but:
The conversion to html format means I will lose custom attributes. Any clean solution will be gladly accepted.

File UIPasteboard+AttributedString.h:

@interface UIPasteboard (AttributedString)

- (void) setAttributedString:(NSAttributedString *)attributedString;

@end

File UIPasteboard+AttributedString.m:

#import <MobileCoreServices/UTCoreTypes.h>

#import "UIPasteboard+AttributedString.h"

@implementation UIPasteboard (AttributedString)

- (void) setAttributedString:(NSAttributedString *)attributedString {
    NSString *htmlString = [attributedString htmlString]; // This uses DTCoreText category NSAttributedString+HTML - https://github.com/Cocoanetics/DTCoreText
    NSDictionary *resourceDictionary = @{ @"WebResourceData" : [htmlString dataUsingEncoding:NSUTF8StringEncoding],
    @"WebResourceFrameName":  @"",
    @"WebResourceMIMEType" : @"text/html",
    @"WebResourceTextEncodingName" : @"UTF-8",
    @"WebResourceURL" : @"about:blank" };



    NSDictionary *htmlItem = @{ (NSString *)kUTTypeText : [attributedString string],
        @"Apple Web Archive pasteboard type" : @{ @"WebMainResource" : resourceDictionary } };

    [self setItems:@[ htmlItem ]];
}


@end

Only implemented setter. If you want to write the getter, and/or put it on GitHub, be my guest :)

Community
  • 1
  • 1
Guillaume
  • 21,685
  • 6
  • 63
  • 95
  • 1
    No use of DTCoreText. Convert this in HTML by this: NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, nil]; \n NSData *htmlData = [attributedString dataFromRange:NSMakeRange(0, attributedString.length) documentAttributes:documentAttributes error:NULL]; \n NSString *htmlString = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding]; – Muzammil Sep 08 '14 at 14:40
  • You can preserve custom attributes and everything else if you use `UIPasteboard.setObjects` and your custom attributes implement `NSSecureCoding`. See here: https://stackoverflow.com/a/76725592/412107 – Tom Hamming Jul 19 '23 at 23:36
1

It is quite simple:

  #import <MobileCoreServices/UTCoreTypes.h>

  NSMutableDictionary *item = [[NSMutableDictionary alloc] init];

  NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
                             documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
                                          error:nil];

  if (rtf) {
    [item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
  }

  [item setObject:attributedString.string forKey:(id)kUTTypeUTF8PlainText];

  UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
  pasteboard.items = @[item];
Valentin Shergin
  • 7,166
  • 2
  • 50
  • 53
0

Converting NSAttributedString to RTF data works for most attributes, but some things like NSTextList won't fully survive the round-trip - the markerFormat property gets muddled. But NSAttributedString implements NSItemProviderWriting, so you can give it directly to the pasteboard via setObjects:

//given `textView`, a `UITextView`:
let range = textView.selectedRange
let selected = textView.textStorage.attributedSubstring(from: range)
UIPasteboard.general.setObjects([selected])

Based on my quick testing, if you have any custom attributes, they'll transfer as long as the types of their values implement NSSecureCoding (or maybe just NSCoding). Sadly, if you're working in Swift, supporting Codable isn't enough.

Tom Hamming
  • 10,577
  • 11
  • 71
  • 145
-1

The pasteboard manager in OSX can auto convert between a lot of textual and image types.

For rich text types, you'd usually place RTF into the pasteboard. You can create RTF representation from an attributed string, and vice versa. See the "NSAttributedString Application Kit Additions Reference".

If you have images included as well, then use the RTFd instead of RTF flavor.

I don't know the MIME types for these (I'm used to the Carbon Pasteboard API, not the Cocoa one), but you can convert between UTIs, Pboard and MIME Types using the UTType API.

UTI for RTF is "public.rtf", for RTFd it's "com.apple.flat-rtfd".

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
  • Oops - that little "ios" tag is easy to overlook. And iOS doesn't appear to have RTF conversion built in. Bummer. – Thomas Tempelmann Mar 12 '13 at 11:41
  • Agreed. Usually, when the tag is the only way to see it, I explicit it in the question. I didn't because I thought UIPasteboard would be enough. Sorry for not being more clear. – Guillaume Mar 12 '13 at 16:23
  • Oh, UIPasteboard. Yes, I could've noticed that one. I work so much on iOS and OSX that I keep confusing their slightly differently named classes all the time :) – Thomas Tempelmann Mar 12 '13 at 21:26