騒音のない世界 BLOG

コンピュータと、音楽と。

UIPasteboardをもっと理解する (後編)

「UIPasteboardをもっと理解する」の後編です。前編はこちらになります。

UIPasteboardをもっと理解する (前編) - 騒音のない世界 BLOG

後編は具体的な動作例とハマりがちな注意点について見ていきます。

具体的な動作を見る

ペーストボードの中身をログに吐いて、具体的な動作を見ていきます。環境はシミュレータ(10.2)です。ログに吐くコードはこんな感じです。

func printPb(_ pb: UIPasteboard) {
    for item in pb.items {
        for (key, value) in item {
            print("\(key): \(value)")
        }
    }
}

UITextViewやUITextFieldからテキストをコピー

画面を長押しして出るメニューからCopyを選んでHogeという文字をコピーします。

f:id:a_beco:20170124041619p:plain

public.utf8-plain-text: Hoge
com.apple.flat-rtfd: <72746664 00000000 ...>
Apple Web Archive pasteboard type: <3c21444f 43545950 ...>

複数の値が入りました。1つずつ見ていきます。

最初のpublic.utf8-plain-textはutf-8エンコーディングのプレーンテキストのようです。エンコーディングごとにUTIが違うことがわかります。2つめのrtfdはRich Text Format Directoryというやつで、画像なども含められるAppleのリッチテキストフォーマットです。3つめのWeb ArchiveというのはSafariでつかうフォーマットで、htmlや画像、音などリソースも含めたウェブページを保存するフォーマットです。

このように、標準のUITextViewなどからコピーすると複数のフォーマットでコピーされることがあります。

写真アプリでコピー

標準の写真アプリで写真をコピーしてみます。

com.apple.mobileslideshow.asset.localidentifier: <31303645 39394131 ...>
public.jpeg: <UIImage: 0x608000092390>, {4288, 2848}

こちらも複数の値が入りました。

最初のものはよくわかりませんが、com.apple.mobileslideshowというのが写真アプリのバンドルIDなので、おそらく写真アプリの内部でのみ用いるデータフォーマットかと思われます。2つめは単にjpegフォーマットです。

また、テキスト、写真ともにUIからコピーするとそれまで保持していた値はクリアされるようです。

コードでコピー

コードから画像と文字列をコピーしてみます。

print("===")
UIPasteboard.general.image = UIImage(named: "sample")
printPb(UIPasteboard.general)
print("===")
UIPasteboard.general.string = "Hoge"
printPb(UIPasteboard.general)
print("===")

結果はこうなりました。

===
public.jpeg: <UIImage: 0x6080002809b0>, {512, 512}
com.apple.uikit.image: <UIImage: 0x60800009fd10>, {170.66666666666666, 170.66666666666666}
public.png: <UIImage: 0x60000009f630>, {512, 512}
===
public.utf8-plain-text: Hoge
===

imageプロパティに画像をセットするとjpeg, pngに加えてcom.apple.uikit.imageというものがコピーされました。元データのフォーマットによらず複数のフォーマットでコピーされます。ドキュメントを見てもpngとjpgで保存される旨が書いてあります。

テキストは単にセットするとUTF8のプレーンテキストになりました。

[2017.03.30 追記] iOS8系, 9系だとimageプロパティに画像をセットした時にcom.apple.uikit.imageしかコピーされません。この場合iOS8系のメモ帳アプリに貼り付けられませんでした。他にも貼り付けられないアプリがあるかもしれないので、直接itemsプロパティに代入したほうが良いかもしれません。

ペースト先の処理を考える

コピーしたデータをUI操作によりペーストできるかどうかは、ペースト先のアプリが対応しているUTIにより決まります。ペースト先のアプリはペースト操作が実行されるとペーストボードから対応UTIのデータ取得を試み、あればそのデータを取得してUIに反映します。

ドキュメントにも注意点として書かれていますが、自分でコピー処理を実装する場合、ペースト先のことを考えてさまざまなフォーマットでコピーしておくと親切です。例えば、画像をコピーするためにpublic.pngにのみ保存していると、public.jpegにしか対応していないアプリにはペーストできないことになり、ユーザが混乱してしまう可能性があるので注意が必要です。UITextViewや写真アプリでコピーした際に複数のフォーマットでコピーされるのもそのような理由があるものと思われます。

間違えがちなポイント

特定フォーマットの存在チェック

ドキュメントによると、stringプロパティやimageプロパティのnilチェックによってペーストボード内にデータがあるかどうかをチェックしてはいけません。代わりにiOS10から追加されたhasStringsなどを使うと良いようです。これはリソースを無駄に消費しないようにするためのようです。

注意点として、hasStringsstringの有無ではなくstringsの有無と対応する、ということがあります。stringは1つめのPasteboard Itemからしか文字列を返さないのに対し、stringsはペーストボード内のすべてのPasteboard Itemからすべての文字列を返します。なので2つめ以降のPasteboard Itemに文字列が入っている場合、hasStringsがtrueでもstringはnilになることがあります。下記のコードで再現します。

let pb = UIPasteboard.general
        
pb.image = UIImage(named: "sampleImage") // 1つめのPasteboard Itemは画像を入れる
pb.addItems([["public.utf8-plain-text" : "hoge"]]) // 2つめのPasteboard Itemは文字列を入れる
        
print("hasStrings: \(pb.hasStrings)")
print("string: \(pb.string)")

結果

hasStrings: true
string: nil

stringの有無を調べたければcontains(pasteboardTypes: [String])を使うと良いです。こちらは先頭のPasteboard Itemしかチェックしません。文字列のUTIはUIPasteboardTypeListStringという配列から取得できるので、contains(pasteboardTypes: UIPasteboardTypeListString as! [String])のように書けます。先程の例に適用すると、この戻り値はfalseとなります。

ペーストボードからデータ削除

UIPasteboard deleteなどで検索すると空文字をセットするコードがたくさん出てきますが、これはあまりよくないかと思います。細かい話ではありますが、空文字は文字列としてペーストボードに保存されてしまうため、UITextViewで長押しした際のメニューで無駄にPasteが出てしまいます。

f:id:a_beco:20170124035844p:plain

データ削除はitemsプロパティを直接操作することで可能です。全て削除する場合は単にUIPasteboard.general.items.removeAll()とすれば良いです。先程のメニューはこうなります。

f:id:a_beco:20170124040000p:plain

おわりに

2記事に渡ってUIPasteboardについてまとめてみましたが、いかがでしたでしょうか。何か新しい発見があれば幸いです。