ObjecTips

Swift & Objective-C で iOS とか macOS とか

UITextView で「リッチテキストとしてペースト」pasteAsRichText を実装する

OS Xでの挙動

OS XNSTextView には

- (void)paste:(id)sender;
- (void)pasteAsPlainText:(id)sender;
- (void)pasteAsRichText:(id)sender;

これらのメソッドがあって、システムのペーストの挙動をそのまま使う事もできるしプレーンテキストやリッチテキストでペーストをする事もできる。
編集メニューの「ペースト」使った場合は paste: メソッドが呼ばれてペーストボード上にある NSAttributedString がそのままペーストされる。動きとしては pasteAtRichText: メソッドと同じになる。
編集メニューの「ペーストしてスタイルを合わせる」Paste and Match Style を使った場合は pasteAsPlainText: メソッドが呼ばれ、テキストビューのキャレットの位置のアトリビュートが反映された文字列がペーストされる。 *1

iOSでの挙動

iOSUITextView では標準では

- (void)paste:(id)sender;

が使える。
このメソッドにペースト処理を任せた場合、例えば以下のように黒字と赤字の部分をまとめてコピーしてテキストの末尾にペーストすると赤文字でペーストされる。

ここでコピー
f:id:Koze:20160208091604p:plain

ここでペースト
f:id:Koze:20160208091606p:plain

結果
f:id:Koze:20160208091607p:plain

OS Xでいう「ペーストしてスタイルを合わせる」Paste and Match Style と同じ挙動になるので、iOSOS Xとではデフォルトのペーストの挙動が違うという事になる。
そこでiOSの方に足りていない、コピーしたリッチテキスト (NSAttributedString) をそのままペーストするための pasteAsRichText: メソッドを実装する。

実装

まずポップアップメニュー表示のメニュー一覧に独自のメソッドが表示されるようメニューの追加を任意のタイミングで行っておく。

    UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@"Paste As Rich Text" action:@selector(pasteAsRichText:)];
    [UIMenuController sharedMenuController].menuItems = @[menuItem];

そして設定した pasteAsRichText: メソッドへの対応を UITextView のサブクラスで実装する。

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    if (action == @selector(pasteAsRichText:)) {
        UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
        return [pasteboard containsPasteboardTypes:@[(NSString *)kUTTypeFlatRTFD]];
    }
    return [super canPerformAction:action withSender:sender];
}

コピー操作後のペーストボードの pasteboardTypes を確認すると

(
    "com.apple.flat-rtfd",
    "public.utf8-plain-text",
    "Apple Web Archive pasteboard type"
)

の3つが入っていたので com.apple.flat-rtfd のタイプを示す kUTTypeFlatRTFD を使用している。
この定義は MobileCoreServices.framework のヘッダの載っていて kUTTypeFlatRTFD Flattened RTFD (pasteboard format) と書かれているので、ペーストボード用のRTFDフォーマットらしいという事が分かる。

canPerformAction:sender: メソッドの実装内容としては、このタイプのデータがペーストボードに入っていれば "Paste As Rich Text"のメニューが有効になる(表示される)ようにしている。

そして実際にペーストを行うメソッド

- (void)pasteAsRichText:(id)sender
{
    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
    NSData *data = [pasteboard dataForPasteboardType:(NSString *)kUTTypeFlatRTFD];
    NSAttributedString *aString = [[NSAttributedString alloc] initWithData:data
                                                                   options:nil
                                                        documentAttributes:nil
                                                                     error:nil];
    NSRange selectedRange = self.selectedRange;
    [self.textStorage replaceCharactersInRange:selectedRange withAttributedString:aString];
    // ペースト後にキャレット位置を移動
    self.selectedRange = NSMakeRange(selectedRange.location + aString.length, 0);
}

ペーストボードから kUTTypeFlatRTFD で取り出せるデータは NSData なので、そこから NSAttributedString を作って UITextView にペーストする。
ペーストの際には単純に追加するのではなく、現在の選択範囲とペースト後の選択範囲(キャレットの位置)を考慮する。

ここまでの実装で "Paste As Rich Text" を実行

f:id:Koze:20160208092432p:plain

これはいけない。コピー元のテキストのアトリビュートが正しく反映されていない。

NSData から作成した NSAttributedString の中身を確認すると

pariatur. Excepteur{
    NSFont = "<UICTFont: 0x7f9072c55d00> font-family: \"Helvetica\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 36, Blocks (null), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0";
}

となっていてコピー元のアトリビュートと変わっていた。
おそらく kUTTypeFlatRTFD やデフォルトの copy: 操作の挙動の仕様だろうという事で copy: から挙動をカスタマイズする必要がありそう。

という事で次回に続く。

*1:データとしてはアトリビュートが無いテキストがペースト追加されているけど、見た目上の結果としてアトリビュートが無いテキストが追加されるわけではない事に若干注意