📄

iOSでWebViewをPDF化して保存する

2023/09/01に公開

初めに

初めまして、2022年9月よりスペースマーケットでモバイルアプリエンジニアをしている王と申します。以後、よろしくお願いします。

弊社ではあらゆるスペースを時間単位で貸し借りできるスペースのブラットフォームを自社で開発、運営を行っております。

中には法人でご利用のお客様が多く、利用前の見積書や利用後の領収書をアプリからでも保存したいというご意見をたくさんいただいております。

このニーズに対して、今回は見積書や領収書のようなWebViewで表示する画面をPDF化し、ローカルストレージに保存する機能を実装しましたので、実装方法(iOS編)をご紹介できればと思います!(Android編はこちら)

最新版アプリにてご利用できますので、ぜひこちらのリンクよりダウンロードして、普段と違う空間を満喫してください🎄

目標

スペース予約画面で「見積書を発行」をクリックすると

WebViewの見積書が表示され、

右上のボタンを押すと、
PDF化したファイルを保存・共有する画面が出てきます

実装方法

忙しい方へ

WebViewController.swift
WebViewController.swift
import UIKit
import WebKit

class WebViewViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
    var url: URL!
    
    private var webView: WKWebView!
    // メソッド内の場合、メモリが解放されてしまうためクラス変数で定義
    private var documentInteractionController: UIDocumentInteractionController?
    private var saveButton: UIBarButtonItem?
    
    override func loadView() {
        setUpWebView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
                // Webが読み込み完了まで保存ボタンを押せないようにする
	saveButton?.isEnabled = false
	setUpNavigationBar()
        loadUrlRequest()
    }
    
    override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        super.webView(webView, didFinish: navigation)
	// Webが読み込み完了したら保存ボタンを活性化
        saveButton?.isEnabled = true
    }
    
    private func setUpWebView() {
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.uiDelegate = self
        view = webView
    }
    
    private func loadUrlRequest() {
        let request = URLRequest(url: url)
        webView.load(request)
    }
    
    private func setUpNavigationBar() {
            let saveButton = UIBarButtonItem(image: UIImage(symbol: .squareAndArrowDown), style: .plain, target: self, action: #selector(didTapSaveReceiptButton(_:))) 
           navigationItem.rightBarButtonItem = saveButton
    }
    
    private func didTapSaveButton(_ sender: UIBarButtonItem) {
        webView.createPDF { [weak self] result in
	// swift5.7からの新しい記法
	// https://github.com/apple/swift-evolution/blob/main/proposals/0345-if-let-shorthand.md
            guard let self else { return }
	    switch result {
	    case .success(let data):
	        let path = (NSTemporaryDirectory() as NSString).appendingPathComponent("見積書.pdf")
                let url = URL(fileURLWithPath: path)
                    do {
                        try data.write(to: url)
                    } catch {
                        ProgressHUD.showError()
                        return
                    }
		    self.documentInteractionController = UIDocumentInteractionController(url: url)
                    self.documentInteractionController?.presentOptionsMenu(from: self.view.frame, in: self.view, animated: true)
	    case .failure:
	        ProgressHUD.showError()
	    }
        }
    }
}

WebViewを表示する

まずはwebの表示から見ていきましょう

WebViewViewController.swift
class WebViewViewController: UIViewController, WKUIDelegate {
    var webView: WKWebView!
    
    override func loadView() {
        setUpWebView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
	setUpNavigationBar()
        loadUrlRequest()
    }
}

webConfigurationを生成したり、delegate登録したりなど初期の設定をこのメソッドで行います

private func setUpWebView() {
    let webConfiguration = WKWebViewConfiguration()
    webView = WKWebView(frame: .zero, configuration: webConfiguration)
    webView.uiDelegate = self
    view = webView
}

指定のURLをWebViewにロードします

private func loadUrlRequest() {
        let url = URL(string: L10n.url)
    let request = URLRequest(url: myURL!)
    webView.load(request)
}

PDF化したWebViewを保存するため、NavigationBarにボタンを追加します

private func setUpNavigationBar() {
        // didTapSaveReceiptButtonは後ほど説明
        let saveButton = UIBarButtonItem(image: UIImage(symbol: .squareAndArrowDown), style: .plain, target: self, action: #selector(didTapSaveReceiptButton(_:))) 
       navigationItem.rightBarButtonItem = saveButton
}

WebView画面をPDF化する

いよいよ本題に入りましたね。
AppleがiOS14からcreatePDFという新しいWebKitのAPIがありますので、こちらを利用します(公式ドキュメントはこちら)

private func didTapSaveButton(_ sender: UIBarButtonItem) {
    webView.createPDF { [weak self] result in
        guard let self else { return } // swift5.6からの新しい記法
	switch result {
	case .success(let data):
	    let path = (NSTemporaryDirectory() as NSString).appendingPathComponent("見積書.pdf")
            let url = URL(fileURLWithPath: path)
                do {
                    try data.write(to: url)
                } catch {
                    self.showError()
                    return
                }
		・・・
	case .failure:
	    self.showError()
	}
    }
}

PDFの保存・共有

swiftでのファイル保存はFileManagerが一般的ですが、アプリ内で保存したファイルの閲覧機能がないと、対応するプリンターならその場で印刷できる点から、UIDocumentInteractionControllerを使用することになります。
https://developer.apple.com/documentation/uikit/uidocumentinteractioncontroller

UIDocumentInteractionControllerはiOS3の時代から存在しており、主にアプリ間のファイル連携やファイル送信で用いられています。

WebViewViewController.swift
class WebViewViewController: UIViewController, WKUIDelegate {
    // メソッド内で定義するとメモリが解放されてしまうためクラス変数として定義
    private var documentInteractionController: UIDocumentInteractionController?
    ・・・
}

そして先のdidTapSaveButton()に戻ってUIDocumentInteractionControllerを表示させます

private func didTapSaveButton(_ sender: UIBarButtonItem) {
    webView.createPDF { [weak self] result in
        guard let self else { return } // swift5.6からの新しい記法
	switch result {
	case .success(let data):
	    let path = (NSTemporaryDirectory() as NSString).appendingPathComponent("見積書.pdf")
            let url = URL(fileURLWithPath: path)
                do {
                    try data.write(to: url)
                } catch {
                    self.showError()
                    return
                }
+ 		self.documentInteractionController = UIDocumentInteractionController(url: url)
+               self.documentInteractionController?.presentOptionsMenu(from: self.view.frame, in: self.view, animated: true)
	case .failure:
	    self.showError()
	}
    }
}

完成🎉

全体像

WebViewController.swift
import UIKit
import WebKit

class WebViewViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
    var webView: WKWebView!
    var url: URL!
    
    private var saveButton: UIBarButtonItem?
    
    override func loadView() {
        setUpWebView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
                // Webが読み込み完了まで保存ボタンを押せないようにする
	saveButton?.isEnabled = false
	setUpNavigationBar()
        loadUrlRequest()
    }
    
    override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        super.webView(webView, didFinish: navigation)
	// Webが読み込み完了したら保存ボタンを活性化
        saveReceiptButton?.isEnabled = true
    }
    
    private func setUpWebView() {
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.uiDelegate = self
        view = webView
    }
    
    private func loadUrlRequest() {
        let request = URLRequest(url: url)
        webView.load(request)
    }
    
    private func setUpNavigationBar() {
            let saveButton = UIBarButtonItem(image: UIImage(symbol: .squareAndArrowDown), style: .plain, target: self, action: #selector(didTapSaveReceiptButton(_:))) 
           navigationItem.rightBarButtonItem = saveButton
    }
    
    private func didTapSaveButton(_ sender: UIBarButtonItem) {
        webView.createPDF { [weak self] result in
	// swift5.7からの新しい記法
	// https://github.com/apple/swift-evolution/blob/main/proposals/0345-if-let-shorthand.md
            guard let self else { return }
	    switch result {
	    case .success(let data):
	        let path = (NSTemporaryDirectory() as NSString).appendingPathComponent("見積書.pdf")
                let url = URL(fileURLWithPath: path)
                    do {
                        try data.write(to: url)
                    } catch {
                        self.showError()
                        return
                    }
		    self.documentInteractionController = UIDocumentInteractionController(url: url)
                    self.documentInteractionController?.presentOptionsMenu(from: self.view.frame, in: self.view, animated: true)
	    case .failure:
	        self.showError()
	    }
        }
    }
}
スペースマーケット Engineer Blog

Discussion