プログラムを書こう!

実務や自作アプリ開発で習得した役に立つソフトウェア技術情報を発信するブログ

SwiftでPDFファイルを表示する。

この記事は2019年01月04日に投稿しました。

f:id:paveway:20190914064630j:plain

目次

  1. はじめに
  2. SwiftでPDFファイルを表示する
  3. おわりに

詳解 Swift 第4版

詳解 Swift 第4版

1. はじめに

こんにちは、iOSのエディタアプリPWEditorの開発者の二俣です。
今回はPWEditorで使用しているSwiftでPDFファイルを表示する方法についてです。

目次へ

2. SwiftでPDFファイルを表示する

SwiftでPDFファイルを表示する方法ですが、iOS11以降で利用可能なPDFKitを利用した方法ではない方法を紹介します。

PWEditorで実装したコードをになりますが、以下の機能があります。

  1. PDFファイルを表示します。
  2. 左右のスワイプにより、ページ送り・戻りが可能です。
  3. ピンチイン/ピンチアウトで縮小・拡大(0.1倍〜10倍)表示可能です。
  4. ダブルタップによる拡大表示が可能です。
  5. シングルタップで等倍表示に戻ります。
  6. 画面の向きにより適切にPDFを表示します。

実装例

import UIKit
import CoreGraphics

/**
 PDFビュークラス
 */
class PDFView: UIView {
    // ページ
    var page: CGPDFPage?
    
    /**
     PDFを描画します。
     
     - Parameter rect: 描画する領域
     */
    override func draw(_ rect: CGRect) {
        guard let page = page else {
            return
        }
        
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        
        let rectWidth = rect.size.width
        let rectHeight = rect.size.height
        context.translateBy(x: 0, y: rectHeight)
        context.scaleBy(x: 1.0, y: -1.0)
        
        let box = page.getBoxRect(.artBox)
        let boxWidth = box.size.width
        let boxHeight = box.size.height
        
        let xScale: CGFloat = rectWidth / boxWidth
        let yScale: CGFloat = rectHeight / boxHeight
        let scale = min(xScale, yScale)
        
        let tx: CGFloat = (rectWidth - boxWidth * scale) / 2.0
        let ty: CGFloat = (rectHeight - boxHeight * scale) / 2.0
        
        if UIDevice.current.orientation.isPortrait {
            // 縦向きの場合
            context.translateBy(x: 0, y: ty)
        
        } else {
            // 横向きの場合
            context.translateBy(x: tx, y: 0)
        }
        context.scaleBy(x: scale, y: scale)
        context.drawPDFPage(page)
    }
}
import UIKit

/**
  PDF表示クラス
 */
class PdfViewController: UIScrollViewDelegate {
    
    /// スクロールビュー
    @IBOutlet weak var scrollView: UIScrollView!
    
    // PDFビュー
    @IBOutlet weak var pdfView: PDFView!
    
    /// パス名
    var pathName: String
    
    /// ファイル名
    var fileName: String
    
    /// ページ数
    var pages = 1
    
    /// 現在のページ番号
    var nowPage = 1
    
    /// PDFドキュメント
    var doc: CGPDFDocument!
    
    // MARK: - Initializer
    
    /**
     イニシャライザ
     */
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    /**
     イニシャライザ
     
     - Parameter pathName: パス名
     - Parameter fileName: ファイル名
     */
    init(pathName: String, fileName: String) {
        // 引数のデータを保存します。
        self.pathName = pathName
        self.fileName = fileName
        
        // スーパークラスのメソッドを呼び出します。
        super.init(nibName: nil, bundle: nil)
    }
    
    // MARK: - UIViewController
    
    /**
     インスタンスが生成された時に呼び出されます。
     */
    override func viewDidLoad() {
        // スーパークラスのメソッドを呼び出します。
        super.viewDidLoad()
        
        // 画面名にファイル名を設定します。
        navigationItem.title = fileName
        
        // デリゲートを設定します。
        scrollView.delegate = self
        
        // デフォルトの拡大率を設定します。
        scrollView.zoomScale = 1
        
        // ジェスチャーを追加します。
        addGesture()
        
        // 通知設定を追加します。
        addNotification()
        
        // ファイルデータを取得します。
        getFileData(action: setPdfDoc(_:))
    }
    
    /**
     ジェスチャーを追加します。
     */
    func addGesture() {
        // ダブルタップジェスチャー
        let doubleTapAction = #selector(doubleTap(_:))
        let doubleTapGesture = UITapGestureRecognizer(target: self, action: doubleTapAction)
        doubleTapGesture.numberOfTapsRequired = 2
        pdfView.addGestureRecognizer(doubleTapGesture)
        
        // シングルタップジェスチャー
        let singleTapAction = #selector(singleTap(_:))
        let singleTapGesture = UITapGestureRecognizer(target: self, action: singleTapAction)
        singleTapGesture.numberOfTapsRequired = 1
        singleTapGesture.require(toFail: doubleTapGesture)
        pdfView.addGestureRecognizer(singleTapGesture)
        
        // 左スワイプジェスチャー
        let leftSwipeAction = #selector(leftSwipe(_:))
        let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: leftSwipeAction)
        leftSwipeGesture.direction = .left
        pdfView.addGestureRecognizer(leftSwipeGesture)
        
        // 右スワイプジェスチャー
        let rightSwipeAction = #selector(rightSwipe(_:))
        let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: rightSwipeAction)
        rightSwipeGesture.direction = .right
        pdfView.addGestureRecognizer(rightSwipeGesture)
    }
    
    /**
     通知機能を追加します。
     */
    func addNotification() {
        let action = #selector(orientationDidChange(_:))
        let center = NotificationCenter.default
        let name = UIDevice.orientationDidChangeNotification
        center.addObserver(self, selector: action, name: name, object: nil)
    }
    
    /**
     レイアウトされた後に呼び出されます。
     */
    override func viewDidLayoutSubviews() {
        // スーパクラスのメソッドを呼び出します。
        super.viewDidLayoutSubviews()

        // PDFViewのサイズをScrollViewのサイズに合わせます。
        let width = scrollView.frame.width
        let height = scrollView.frame.height
        let pdfSize = CGSize(width: width, height: height)
        pdfView.frame.size = pdfSize
        scrollView.contentSize = pdfView.frame.size
        
        // PDFViewのinsetもScrollViewのサイズに合わせます。
        let pdfInset = UIEdgeInsets(top: 0, left: 0, bottom: height, right: width)
        pdfView.frame.inset(by: pdfInset)
        
        // contentInsetを更新します。
        updateScrollInset()
    }
    
    /**
     画面が閉じられた後に呼び出されます。
 
     - Parameter animated: アニメーション指定
     */
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        // 通知設定を削除します。
        removeNotification()
    }
    
    /**
     通知機能を削除します。
     */
    func removeNotification() {
        let center = NotificationCenter.default
        center.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
    }
    
    /**
     画面が回転した時に呼び出されます。
     */
    @objc func orientationDidChange(_ notification: NSNotification) {
        // PDFビューを更新します。
        for subView in scrollView.subviews {
            subView.setNeedsDisplay()
        }
    }
    
    /**
     スクロールビューでズームした時に呼び出されます。
     
     - Parameter scrollView: スクロールビュー
     - Returns: ズームされるビュー
     */
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return pdfView
    }
    
    /**
     スクロールビューのズーム倍率が変更された時に呼び出されます。
     
     - Parameter scrollView: スクロールビュー
     */
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        // ズームのタイミングでcontentInsetを更新します。
        updateScrollInset()
    }
    
    /**
     ScrollViewのcontentInsetを更新します。
     */
    func updateScrollInset() {
        // PDFViewの大きさからcontentInsetを再計算します。
        // 0を下回らないようにします。
        let top = max((scrollView.frame.height - pdfView.frame.height) / 2, 0.0)
        let left = max((scrollView.frame.width - pdfView.frame.width) / 2, 0.0)
        scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: 0.0, right: 0.0);
    }
    
    /**
     左スワイプした時に呼び出されます。
     
     - Parameter sender: ジェスチャーオブジェクト
     */
    @objc func leftSwipe(_ sender: UISwipeGestureRecognizer) {
        if nowPage == pages {
            // 最終ページの場合、何もせず終了します。
            return
        }
        
        // 現在のページ数を更新し、ページを変更します。
        nowPage += 1
        changePage()
    }
    
    /**
     右スワイプした時に呼び出されます。
     
     - Parameter sender: ジェスチャーオブジェクト
     */
    @objc func rightSwipe(_ sender: UISwipeGestureRecognizer) {
        if nowPage == 1 {
            // 先頭ページの場合、何もせず終了します。
            return
        }
        
        // 現在のページ数を更新し、ページを更新します。
        nowPage -= 1
        changePage()
    }
    
    /**
     ページを変更します。
     */
    func changePage() {
        let page = doc.page(at: nowPage)
        pdfView.page = page
        for subView in scrollView.subviews {
            subView.setNeedsDisplay()
        }
    }
    
    /**
     シングルタップした時の処理を行います。
     
     - Parameter sender: タップジェスチャーオブジェクト
     */
    @objc func singleTap(_ sender: UITapGestureRecognizer) {
        // ズーム倍率を1倍に設定します。
        scrollView.setZoomScale(1.0, animated: true)
    }
    
    /**
     ダブルタップした時の処理を行います。
     
     - Parameter sender: タップジェスチャーオブジェクト
     */
    @objc func doubleTap(_ sender: UITapGestureRecognizer) {
        if (scrollView.zoomScale < scrollView.maximumZoomScale) {
            // 現在のズーム倍率が最大ズーム倍率より小さい場合
            let scale = scrollView.zoomScale * 2
            let center = sender.location(in: sender.view)
            let rect = zoomRectForScale(scale: scale, center: center)
            scrollView.zoom(to: rect, animated: true)
            
        } else {
            // 現在のズーム倍率が最大ズーム倍率の場合
            // ズーム倍率を1倍に設定します。
            scrollView.setZoomScale(1.0, animated: true)
        }
    }
    
    /**
     スケールに合わせたズームされた領域を設定します。
     
     - Parameter scale: スケール
     - Parameter center: 中央位置
     - Returns: ズームされた領域
     */
    func zoomRectForScale(scale: CGFloat, center: CGPoint) -> CGRect{
        let frameSize = scrollView.frame.size
        let width = frameSize.width / scale
        let height = frameSize.height / scale
        let size = CGSize(width: width, height: height)
        
        let x = center.x - size.width / 2.0
        let y = center.y - size.height / 2.0
        let origin = CGPoint(x: x, y: y)
        
        let rect = CGRect(origin: origin, size: size)
        return rect
    }
    
    /**
     PDFドキュメントを設定します。
     
     - Parameter data: PDFデータ
     */
    @objc func setPdfDoc(_ data: Data) {
        // PDFドキュメントを生成します。
        guard let provider = CGDataProvider(data: data as CFData) else {
            return
        }
        guard let doc = CGPDFDocument(provider) else {
            return
        }
        self.doc = doc
        
        // 総ページ数を取得します。
        pages = doc.numberOfPages
        
        // 現在のページを設定し、表示します。
        let page = doc.page(at: nowPage)
        pdfView.page = page
        pdfView.backgroundColor = UIColor.white
        for subView in scrollView.subviews {
            subView.setNeedsDisplay()
        }
    }
    
    // MARK: - Override
    
    /**
     ファイルデータを取得します。
     サブクラスで実装します。
     ※PWEditorでこのクラスは、各ストレージで共通にしています。
      そのため各ストレージごとに異なるファイルデータの取得処理は
      各ストレージごとのサブクラスで実装するようにしています。
      
      - Parameter action: ファイルデータ取得後に呼び出すクロージャー
     */
    func getFileData(action: @escaping (_ data: Data) -> Void) {
        // デフォルトはエラーにします。
        abort()
    }

目次へ

3. おわりに

実装には以下のサイトを参考にさせていただきました。
ありがとうございます。

【iOS開発】Swiftで簡易PDFビューアーを作成(PDFを読み込み、表示)

画像をダブルタップとピンチイン・ピンチアウトで拡大・縮小する Swift3編

お仕事決まれば全額キャッシュバック!転職特化型Ruby実践研修【ポテパンキャンプ】

詳細! Swift iPhoneアプリ開発入門ノート iOS12 + Xcode 10対応

詳細! Swift iPhoneアプリ開発入門ノート iOS12 + Xcode 10対応

紹介している一部の記事のコードはGitlabで公開しています。
興味のある方は覗いてみてください。

目次へ


私が勤務しているニューラルでは、主に組み込み系ソフトの開発を行っております。
弊社製品のハイブリッドOS Bi-OSは高い技術力を評価されており、特に制御系や通信系を得意としています。
私自身はiOSモバイルアプリウィンドウズアプリを得意としております。
ソフトウェア開発に関して相談などございましたら、お気軽にご連絡ください。

また一緒に働きたい技術者の方も随時募集中です。
興味がありましたらご連絡ください。

EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688

目次へ