Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

iOS アプリで様々なファイルをプレビューできる QLPreviewController の紹介

こんにちは、Sansan プロダクト開発部 iOS アプリエンジニアの相川です。最近個人的に機械学習を勉強していて、数学の勉強もすることになり、毎日新しいことを学ぶことができているのと大学時代に塾講師をしていた経験も少し活きていて楽しいです。

今回は iOS アプリにおいてファイルを簡単にプレビューできる API である QLPreviewController について紹介しようと思います。

QLPreviewController を利用した業務について

Sansan アプリにはコンタクト(商談記録のようなもの)を作成できる機能があります。もう夏頃にはなりますが、アプリ内でコンタクトに添付ファイルを付けたり、コンタクトの添付ファイルを閲覧できるようにしたい!という要望がありました。QLPreviewController はそのうちの 添付ファイル閲覧機能 を実装するために利用することとなりました。

ちなみに実装した機能は ↓ のようなものになります。

f:id:kalupas:20201127105605g:plain:w300
Sansan のコンタクト添付ファイル閲覧機能

プログレスバーは QLPreviewController の機能ではありませんが、pdf ファイルを QLPreviewController でプレビューすることによって、pdf を見ることができるだけではなく、ピンチインピンチアウトなどの動作も実現することができていることがわかります。

ちなみに、この機能を実装する際、ファイルプレビューについてはこれから説明する QLPreviewController を利用することによって簡単に実現することができたのですが、Alamofire を利用してプログレスバーを実現しようとしたら非常に苦労した経緯があったので、いつかその話も紹介できたらと思います。

QLPreviewController について

QLPreviewController については公式が一番よくまとまっていると思います。
以下で紹介する内容も WWDC2018 の「Quick Look Previews from the Ground Up」と Apple Documentation の QLPreviewController についての説明を参考にしています。

developer.apple.com

developer.apple.com

冒頭で少し紹介しましたが、QLPreviewController は iOS アプリ上で様々な種類のファイルをプレビューすることができる API になります。
QLPreviewController の凄さを知って頂くためにも、以下にこの API でプレビューできるファイルの種類を示します。

f:id:kalupas:20201105171918p:plain
QLPreviewController でプレビューできるファイル(WWDC2018 より)

基本的な画像や動画はもちろん、音声ファイル、iWork・Office Documents、さらには QLPreviewExtension というものを利用すればカスタムファイルまでプレビューすることができるものになります。(個人的には AR などで使用されている USDZ ファイルを Sansan 上で開くことができるようになったことが面白かったです。誰も USDZ ファイルなんて添付しないと思いますが...)

QLPreviewController を利用すれば、開発者は大したコードを書くことなくファイルをアプリ上でプレビューすることができるようになります。
しかも、ファイル形式ごとに最適化したプレビューで表示してくれるようになっています。具体的には、以下のように表示されます。

  • 画像ファイル:ピンチイン・ピンチアウトができる
  • PDF ファイル:スクロール中に PDF のページ数が表示される
  • 音声・動画ファイル:Scrubber(横にスライドして早送りしたり巻き戻しできる UI )が付いている
  • スプレッドシート(Excel):スプレッドシート内にタブがある場合は、タブ移動もできる
  • 各種ファイル:スワイプで dismiss できる

他のファイルもそれなりに適切に表示してくれることに加え、UIActivityViewController のようなものも標準で付属しているため QLPreviewController は非常に扱いやすいと思います。また、WWDC の動画内では発信元が不確かなファイルである場合も Quick Look がアプリケーションを守ってくれると述べられていました。(どのように守ってくれるのかは述べられていませんでしたが...)

ここまで QLPreviewController のメリットを説明しましたが、もちろんデメリットも存在します。WWDC の動画内で挙げられているデメリットは以下になります。

  • プレビュー専用で編集機能はない(画像編集やPDF文書の管理などが必要なら別の API を利用するべき)
  • 高度な動画再生方法が必要であれば AVPlayer を利用するべき
  • 画像をフルスクリーンで表示するので他の View と合わせて利用することはできない
  • 他の View と合わせて利用することはできず、UI のカスタマイズも基本的にはできない(ナビゲーションバーのタイトルは変更できる)

今回 Sansan で QLPreviewController を利用するにあたって唯一問題となったのは 4 つ目の条件の「他の View と合わせて利用することはできず、UI のカスタマイズも基本的にはできない」というものでした。Sansan アプリではナビゲーションバー上に専用のクローズボタンを利用しており、QLPreviewController のクローズボタンも同じデザインにしたかったのですが、カスタマイズできないという制約上、そちらに関しては諦めることとなりました。しかし、それを補う便利さを感じていたため、QLPreviewController を利用して実装することにしました。

諦めたと言っても何とかカスタマイズできないかなと試行錯誤していた経緯はあり、その際に QLPreviewController のナビゲーションバー上のシェアボタンを消すことができるらしいという Stack Overflow の回答に出会いました。
しかし、その方法は結構無理やりな実装だったり、View を壊してしまう(ゆえに iPad だと iPhone では見えないはずのシェアボタンが表示されてしまう)方法だったため、やはり QLPreviewController を利用する場合は UI のカスタマイズはできないと思った方が良さそうです。 stackoverflow.com

次節では実際に QLPreviewController の利用方法を説明していきます。

QLPreviewController の利用方法について

以下のような簡単なサンプル(今回は png を 5種類、mp3 を 1 種類、pdf を 1 種類 QLPreviewController で表示する)を用いて利用方法について説明します。(コードはこちら

f:id:kalupas:20201122124008g:plain:w300
QLPreviewController のサンプルアプリ

QLPreviewController を利用するための大きな流れとしては以下になります。

  • 遷移元の ViewController を QLPreviewControllerDataSource プロトコルに適合させる
  • (オプション: 遷移元の ViewController を QLPreviewControllerDelegate プロトコルに適合させる)
  • 遷移元の ViewController 上で QLPreviewController を present(or push) する

以下では上記の流れに沿って説明していきます。

遷移元の ViewController を QLPreviewControllerDataSource プロトコルに適合させる

いきなりプロトコルに適合させる前に今回用意したデータについて軽く触れておきます。

let previewItemNameList = [("food_kani_guratan_koura", "png"),
                           ("movie_refuban_man", "png"),
                           ("music_castanet_girl", "png"),
                           ("school_tsuugaku_woman", "png"),
                           ("syoujou_kaikinsyou", "png"),
                           ("bgm_maoudamashii_fantasy15", "mp3"),
                           ("sample-pdf", "pdf")]

var previewItemURLList: [URL] {
    let itemURLList = previewItemNameList.map { Bundle.main.url(forResource: $0.0, withExtension: $0.1)! }
    return itemURLList
}

↑ のようにタプルでファイル名と拡張子を定義し、そこから URL のリストを作っているだけになります。ファイルはルートディレクトリ直下に全て配置しています。
このデータを使用して QLPreviewController を実際に表示させていきます。
QLPreviewController を表示するためにはまず QLPreviewControllerDataSource プロトコルに適合させる必要があります。
プロトコルは以下のようなものになります。

protocol QLPreviewControllerDataSource {
    // プレビューしたい項目の個数を返す。項目が二つ以上あればスワイプ機能なども使用できる
    func numberOfPreviewItems(in controller: QLPreviewController) -> Int
    // 複数ファイルをプレビューする場合は index で QLPreviewItem を返すように指定する
    func previewController(_ controller: QLPreviewController,
                           previewItemAt index: Int) -> QLPreviewItem
}

今回は遷移元の ViewController に対して以下のように適合させました。

extension ViewController: QLPreviewControllerDataSource {
    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        return previewItemURLList.count
    }
    
    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        return previewItemURLList[index] as QLPreviewItem
    }
}

numberOfPreviewItems(in: ) -> Int では今回定義しているファイル数を返すようにし、 previewController(_:previewItemAt: ) -> QLPreviewItem ではindex ごとのファイル URL を QLPreviewItem として返すようにしているだけになります。

以上で「遷移元の ViewController を QLPreviewControllerDataSource プロトコルに適合させる」は完了しました。

(オプション:遷移元の ViewController を QLPreviewControllerDelegate プロトコルに適合させる)

次は、適合させることが必須ではないものになるのですが、時々使用することはあると思うため少しだけ紹介しておきます。

protocol QLPreviewControllerDelegate {
    optional func previewControllerWillDismiss(_ controller: QLPreviewController)

    optional func previewControllerDidDismiss(_ controller: QLPreviewController)

    optional func previewController(_ controller: QLPreviewController, 
                                                          shouldOpen url: URL, 
                                                          for item: QLPreviewItem) -> Bool

    optional func previewController(_ controller: QLPreviewController,
                                                          frameFor item: QLPreviewItem,
                                                          inSourceView view: AutoreleasingUnsafeMutablePointer<UIView?>)
                                                          -> CGRect

    optional func previewController(_ controller: QLPreviewController,
                                                           transitionImageFor item: QLPreviewItem,
                                                           contentRect: UnsafeMutablePointer<CGRect>) -> UIImage?

    optional func previewController(_ controller: QLPreviewController,
                                                           transitionViewFor item: QLPreviewItem) -> UIView?
}

おそらく普通に QLPreviewController を利用するだけであれば上三つのメソッドが重要だと思っていて、それぞれ以下のような時に利用することができます。

// QLPreviewController が dismiss し始める時の処理
optional func previewControllerWillDismiss(_ controller: QLPreviewController)
// QLPreviewController が dismiss し終わった後の処理
optional func previewControllerDidDismiss(_ controller: QLPreviewController)
// QLPreviewController 内で表示するファイルに含まれているリンクをタップし、
// ユーザーがアプリ外へ遷移してしまうことを防ぐことができる
optional func previewController(_ controller: QLPreviewController, 
                                shouldOpen url: URL, 
                                for item: QLPreviewItem) -> Bool

他のメソッドに関しては WWDC2018 の「Quick Look Previews from the Ground Up」でも詳しく説明されているため、ここでは割愛します。
今回は上記三つのメソッドのうち、 previewControllerDidDismiss(_: ) をおまけ程度に以下のように実装しています。

extension ViewController: QLPreviewControllerDelegate {
    func previewControllerDidDismiss(_ controller: QLPreviewController) {
        let alert = UIAlertController(title: "DidDismiss", message: nil, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(okAction)
        present(alert, animated: true)
    }
}

単純なアラートを QLPreviewController が dismiss した後で表示しているだけになります。
以上で「(オプション:遷移元の ViewController を QLPreviewControllerDelegate プロトコルに適合させる」も完了しました。

遷移元の ViewController 上で QLPreviewController を present(or push) する

最後は簡単ですが、以下のように実装します。

let previewController = QLPreviewController()
previewController.dataSource = self
previewController.delegate = self
present(previewController, animated: true)

定義した previewController の dataSource と delegate には忘れずに遷移元の ViewController を set してあげます。
以上で今回のサンプルは実装終了になります 🍵

QLPreviewController についての情報源

QLPreviewController はファイルをプレビューするという目的でしか使われない性質上、実装している時は比較的説明されている記事などが少ないと感じていました。
やはり、QLPreviewController を利用して実装を行う際には、最初に紹介した WWDC の動画や公式ドキュメントなどを参照するのが一番良さそうに思います。

WWDC の動画内でも紹介されていましたが、ファイルを扱うという性質のため、下記の動画なども参照すると良さそうです。

developer.apple.com

developer.apple.com

また 2019 年には Quick Look についての動画も出ているので、こちらも参考になりそうです。

developer.apple.com

おわりに

iOS で標準で提供されている API は数多くあり、QLPreviewController のように使いこなすことができれば強力なものも多くあります。
もちろんライブラリを利用したり自分で実装することも必要ではありますが、標準の API も十分に駆使しながら効率的に良いものを作っていきたいと感じました。


buildersbox.corp-sansan.com buildersbox.corp-sansan.com buildersbox.corp-sansan.com

© Sansan, Inc.