iOSとファイルのサムネイル
iOSでは、Mac同様にファイルを扱う機構が存在します。
標準では「ファイル」アプリを通してiCloud Driveやローカルのファイルにアクセス出来ますし、それぞれのアプリではUIDocumentPickerViewController
を使ってアクセスすることも出来ます。
これらのファイルブラウザではjpegやpngなど標準的なファイルフォーマットのサムネイル表示が標準で実装されています。

また、サムネイルの表示に対応していないファイルもQuickLookThumbnailing.framework
を使った拡張機能をアプリにバンドルしてインストールすることでサムネイル表示に対応させることが出来ます。
このエントリでは、iOSにおけるサムネイル拡張機能の実装と予備知識を紹介します。
実装の方法
「Thumbnail Extension」は、アプリの拡張機能に当たるため単体でインストールさせることが出来ません。
メインのアプリにバンドルする形で配布する必要があります。
ここでは、適当にアプリプロジェクトを作成し「Thumbnail Extension」用のターゲットを追加します。

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

ThumbnailProvider.swift
にサムネイル生成の処理を記述し
Info.plist
にはどのファイルに対して処理を行うかを記述します。
Info.plistの設定
Info.plistを開くと次のような構造になっています。

QLSupportedContentTypes
にはカスタムドキュメントのUTIを、QLThumbnailMinimumDimension
にはサムネイルの最小サイズを記述します。
QLSupportedContentTypes
に設定するUTIの調べ方
UTIは次のコマンドで確認することができます。
$ mdls file.ext
今回はSwiftファイルを例に実行した結果を見てみましょう。
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
の中は次のような構造になっています。
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:)
があります。

init(imageFileURL:)
はファイルの構造の中にサムネイル用のファイルが配置されているときなどに適しています。
また、drawing
系はサムネイルの取得時にCGImageを受け取る場合や、文字や画像を合成したいときに利用するのに適しています。
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は必ずサブクラスを作る必要があります。
class CustomDocument: UIDocument {
override func open(completionHandler: ((Bool) -> Void)? = nil) {
completionHandler?(true)
}
}
カスタムクラスを作ったら、次のようにopenのクロージャの中でファイルにアクセスします。
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の設定を見直してみましょう。

参考
利用例
vearではvrmファイルのサムネイルをThumbnail extensionで実装しています。
ThumbnailExtension実装した。50MB制限キツくて必要無いやつはロードしないようにしてやっと動いた。 pic.twitter.com/SyzlmmCQaS
— noppe (@noppefoxwolf) March 8, 2020