[iOS]カスタムドキュメントにサムネイルを表示する

# iOSとファイルのサムネイル

iOSでは、Mac同様にファイルを扱う機構が存在します。
標準では「ファイル」アプリを通してiCloud Driveやローカルのファイルにアクセス出来ますし、それぞれのアプリでは`UIDocumentPickerViewController`を使ってアクセスすることも出来ます。

![IMG_1408.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/38f5f863-fec8-6256-edfe-03f751bf3383.jpeg)

これらのファイルブラウザではjpegやpngなど標準的なファイルフォーマットのサムネイル表示が標準で実装されています。

<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/b5274a27-d54d-9a94-c2bf-235e22145797.png">

また、サムネイルの表示に対応していないファイルも`QuickLookThumbnailing.framework`を使った拡張機能をアプリにバンドルしてインストールすることでサムネイル表示に対応させることが出来ます。
このエントリでは、iOSにおけるサムネイル拡張機能の実装と予備知識を紹介します。

# 実装の方法

「Thumbnail Extension」は、アプリの拡張機能に当たるため単体でインストールさせることが出来ません。
メインのアプリにバンドルする形で配布する必要があります。
ここでは、適当にアプリプロジェクトを作成し「Thumbnail Extension」用のターゲットを追加します。

<img width="1099" alt="スクリーンショット 2020-03-09 20.48.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/2e65b242-b0a7-a284-45fd-dd4ce9753e66.png">

ターゲットを追加すると、ファイルツリーに次のような構造が作成されます。

<img width="272" alt="スクリーンショット 2020-03-09 20.53.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/a05c2849-a9e1-bcc7-c302-5744a866c398.png">

`ThumbnailProvider.swift`にサムネイル生成の処理を記述し
`Info.plist`にはどのファイルに対して処理を行うかを記述します。

## Info.plistの設定

Info.plistを開くと次のような構造になっています。

<img width="612" alt="スクリーンショット 2020-03-09 20.58.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/c70ca4bb-3463-9bb2-8bad-697d741a14c3.png">

`QLSupportedContentTypes`にはカスタムドキュメントのUTIを、`QLThumbnailMinimumDimension`にはサムネイルの最小サイズを記述します。

### `QLSupportedContentTypes`に設定するUTIの調べ方

UTIは次のコマンドで確認することができます。

```
$ mdls file.ext
```

今回はSwiftファイルを例に実行した結果を見てみましょう。

```:output
kMDItemContentType                     = "public.swift-source"
kMDItemContentTypeTree                 = (
    "public.swift-source",
    "public.source-code",
    "public.plain-text",
    "public.text",
    "public.data",
    "public.item",
    "public.content"
)
```

`QLSupportedContentTypes`には`kMDItemContentTypeTree`に表示されているタイプを**全て**記入します。
`kMDItemContentType`だけだと上手く処理が呼ばれないことが多いです。

### `QLThumbnailMinimumDimension`の値

基本的には弄る必要は無いかと思います。デフォルトは17です。

## `QLThumbnailProvider`の実装

`ThumbnailProvider.swift`の中は次のような構造になっています。

```swift
import UIKit
import QuickLookThumbnailing

class ThumbnailProvider: QLThumbnailProvider {
    override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
        handler(QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
            return true
        }), nil)
    }
}
```

構造としては非常にシンプルで、provideThumbnailを実装するだけです。

`QLFileThumbnailRequest`からは入力されたファイルパスなどが取れます。
この入力を元に画像を描画する処理を`QLThumbnailReply`に詰めて返します。

`QLThumbnailReply`は

画像のURLを返す`init(imageFileURL:)`
CGContextを受け取る`init(contextSize:, drawing:)`
描画コンテキストが開始されている`init(contentSize: currentContextDrawing:)`
があります。

<img width="815" alt="スクリーンショット 2020-03-09 21.18.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/6285f51a-52c4-9fb5-cdc0-583924340108.png">

`init(imageFileURL:)`はファイルの構造の中にサムネイル用のファイルが配置されているときなどに適しています。

また、`drawing`系はサムネイルの取得時にCGImageを受け取る場合や、文字や画像を合成したいときに利用するのに適しています。

```swift
let thumbnail = ThumbnailLoader(url: url).load()
let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: {
  let frame = CGRect(origin: .zero, size: request.maximumSize)
  thumbnail?.draw(in: frame)
  return true
})
handler(reply, nil)
```

### ファイルの読み込み

`QLFileThumbnailRequest`に入っている`fileURL`をそのまま使ってアクセスすると、アクセスがブロックされることがあります。
fileURLは必ずUIDocumentを介してアクセスする必要があるので、注意する必要があります。
次のように、`UIDocument`を継承したクラスに、openメソッドを実装します。
**super.openを呼ばない点**に注意しましょう。
また、**UIDocumentは必ずサブクラスを作る必要があります**。

```swift
class CustomDocument: UIDocument {
    override func open(completionHandler: ((Bool) -> Void)? = nil) {
        completionHandler?(true)
    }
}
```

カスタムクラスを作ったら、次のようにopenのクロージャの中でファイルにアクセスします。

```swift
class ThumbnailProvider: QLThumbnailProvider {
    override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
        CustomDocument(fileURL: request.fileURL).open { (_) in
            let thumbnail = ThumbnailLoader(url: url).load()
            let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: {
              let frame = CGRect(origin: .zero, size: request.maximumSize)
              thumbnail?.draw(in: frame)
              return true
            })
            handler(reply, nil)
        }
    }
}
```

## `QLThumbnailProvider`の制限

`QLThumbnailProvider`は、**メモリの使用量を50MB以内に抑える必要があります**。
50MBをオーバーすると拡張機能のプロセスは強制終了させられます。
余分なメモリを確保しないよう、Streamやポインタを上手に使ってサムネイルを生成する処理を書きましょう。


# デバッグ 

ターゲットをThumbnail Extensionに設定し、ビルドするとデバッグすることができます。
もちろんbreak pointを仕込むことも出来ます。
provideThumbnailが呼ばれないようであれば、utiの設定を見直してみましょう。

<img width="940" alt="スクリーンショット 2020-03-09 21.41.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24979/9c45afe3-06d9-2a11-5038-f2de3f060c33.png">


# 参考

https://developer.apple.com/documentation/quicklookthumbnailing/providing_thumbnails_of_your_custom_file_types

# 利用例

[vear](https://apps.apple.com/us/app/vear/id1490697369)ではvrmファイルのサムネイルをThumbnail extensionで実装しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ThumbnailExtension実装した。50MB制限キツくて必要無いやつはロードしないようにしてやっと動いた。 <a href="https://t.co/SyzlmmCQaS">pic.twitter.com/SyzlmmCQaS</a></p>&mdash; noppe (@noppefoxwolf) <a href="https://twitter.com/noppefoxwolf/status/1236649045109530624?ref_src=twsrc%5Etfw">March 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>