iOSでWebViewをPDF化して保存する
初めに
初めまして、2022年9月よりスペースマーケットでモバイルアプリエンジニアをしている王と申します。以後、よろしくお願いします。
弊社ではあらゆるスペースを時間単位で貸し借りできるスペースのブラットフォームを自社で開発、運営を行っております。
中には法人でご利用のお客様が多く、利用前の見積書や利用後の領収書をアプリからでも保存したいというご意見をたくさんいただいております。
このニーズに対して、今回は見積書や領収書のようなWebViewで表示する画面をPDF化し、ローカルストレージに保存する機能を実装しましたので、実装方法(iOS編)をご紹介できればと思います!(Android編はこちら)
最新版アプリにてご利用できますので、ぜひこちらのリンクよりダウンロードして、普段と違う空間を満喫してください🎄
目標
スペース予約画面で「見積書を発行」をクリックすると
WebViewの見積書が表示され、
右上のボタンを押すと、
PDF化したファイルを保存・共有する画面が出てきます
実装方法
忙しい方へ
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の表示から見ていきましょう
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
を使用することになります。
UIDocumentInteractionController
はiOS3の時代から存在しており、主にアプリ間のファイル連携やファイル送信で用いられています。
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()
}
}
}
完成🎉
全体像
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()
}
}
}
}
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion