Part 1 and Part 2 of my text view adventures were about NSTextView
for Mac. Part 3 here and Part 4 in the future will be about UITextView
for iOS. In Part 2 I wrote about how to handle pasting of arbitrary file types, including movies, into NSTextView
. You might wonder how UITextView
handles this. The answer is… not very well. On iOS 12, "Copy" is not even listed among the actions when you share a movie from Photos app. However, you can "Save to Files", and Files app will allow you to copy a movie file. If you want to allow users to save files "On My iPhone" in Files app, you need to add LSSupportsOpeningDocumentsInPlace
and UIFileSharingEnabled
to your app's Info.plist
file.
In a text view, a file attachment is represented as an NSAttributedString
with a special NSAttachmentCharacter
string and a NSAttachmentAttributeName
attribute. This is what you get when you paste an image file, for example. Unfortunately, UITextView
does not support pasting arbitrary file types. It doesn't show movies inline like NSTextView
does. (It doesn't even animate gifs! But that's a story for another day.) When you paste a movie file, you get an NSAttributedString
with an empty string and no attributes, which is totally useless. If you know in advance which file types you want to accept, then you can probably configure the UITextView
to accept them, but what if you want to accept, you know, anything?
Some reverse engineering revealed that paste in UITextView
is handled by UITextInputController
. The empty attributed string for the movie file comes from -[UITextInputController _insertAttributedText:fromKeyboard:]
. I wouldn't be allowed to hack this in the App Store, though, since it's all private system implementation and not public API. To work around the problem, I needed to use supported methods. I couldn't find a good way to reliably identify the paste failure using the UITextView
delegate methods, not even NSTextStorageDelegate
. Thus, I decided to subclass UITextView
and override -[UITextView paste:]
.
Even in -[UITextView paste:]
, you can't really determine when super paste:
fails, and I didn't want to reimplement paste entirely myself, so I decided to let UITextView
handle the types it knows about, and I would handle the rest. Here's some code:
@interface JJPasteTextView : UITextView @end
@implementation JJPasteTextView -(void)paste:(id)sender {
UIPasteboard *generalPasteboard = [UIPasteboard generalPasteboard];
if (![generalPasteboard hasStrings] && ![generalPasteboard hasImages] && ![generalPasteboard hasURLs]) {
NSArray<NSString *> *pasteboardTypes = [generalPasteboard pasteboardTypes];
if ([pasteboardTypes count] > 0) {
NSString *pasteboardType = pasteboardTypes[0];
NSData *data = [generalPasteboard dataForPasteboardType:pasteboardType];
if (data != nil && [data length] > 0) {
NSFileWrapper *fileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:data];
if (fileWrapper != nil) {
CFStringRef extension = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)pasteboardType, kUTTagClassFilenameExtension);
if (extension != NULL) {
[fileWrapper setPreferredFilename:[@"Pasted File." stringByAppendingString:CFBridgingRelease(extension)]];
}
NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithData:nil ofType:nil];
[attachment setFileWrapper:fileWrapper];
NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
if (attachmentString != nil) {
NSRange oldSelectedRange = [self selectedRange];
// Put the new selection right after the attachment.
NSRange newSelectedRange = NSMakeRange(oldSelectedRange.location + 1, 0);
[[self textStorage] replaceCharactersInRange:oldSelectedRange withAttributedString:attachmentString];
[self setSelectedRange:newSelectedRange];
[[self delegate] textViewDidChange:self];
return;
}
}
}
}
}
[super paste:sender]; } @end
iOS will show document icons for file types that it recognizes, or a generic icon for file types it doesn't recognize. You may want to customize the icon of the file attachment, because iOS doesn't recognize many file types. You can customize the icon by subclassing NSTextAttachment
and overriding -[NSTextAttachment image]
. Usually this method returns nil
when the NSTextAttachment
has a fileWrapper
, in which case UITextView
falls back to the file extension to select the appropriate icon, but a custom UIImage
returned by -[NSTextAttachment image]
will take precedence.
That's the end of Part 3. Part 4 will discuss everyone's favorite topic: UITextView
scrolling.