iOS 14.5のWKWebViewでBlobオブジェクトをダウンロードする

commits4 min read読了の目安(約4100字

Webブラウザアプリ開発者待望の新機能

iOS 14.5未満のWKWebViewはblob:ではじまるURLのダウンロード、つまりJavaScriptで生成されたBlobオブジェクトのダウンロードにネイティブ対応していませんでした。BlobオブジェクトはWKWebView上のメモリに存在するため、単純にURLを指定するだけではダウンロードできず、以下のようにWKWebViewと連携するJavaScriptを実行してData URLに変換するなどの工夫が必要でした。

2021年4月27日にリリースされたiOS 14.5から、WKWebViewや関連クラスにダウンロード機能を実装するためのAPIがいくつか追加されました。それらを使うことで、Blobオブジェクトのダウンロードもネイティブコードのみで実装可能になりました。

Blobオブジェクトをダウンロードするための手順

以下がXcode 12.5とSwift 5.4で実装する手順です。わかりやすいようにSwiftUIではなくUIKitベースで実装しています。また、各デリゲートメソッドの実装はWKWebViewをサブビューに持つViewControllerクラスで行っています。

なお、ここではBlobオブジェクトのダウンロード元サイトとしてScratch 3.0のエディター画面を使っています。ファイルメニューから「コンピューターに保存する」を選択すると、プロジェクトファイルをsb3の拡張子を持つBlobオブジェクトとしてダウンロードできます。

エディター画面: https://scratch.mit.edu/projects/editor/

Scratch 3.0のエディター画面

手順1. WKNavigationDelegateの実装

WKNavigationDelegateプロトコルのwebView(_:decidePolicyFor:preferences:decisionHandler:)を以下のように実装します。blob:ではじまるURLがリクエストされたときに、decisionHandler()の引数としてWKNavigationActionPolicyに新しく追加された.downloadを返すと、ダウンロードの準備がはじまります。

extension ViewController: WKNavigationDelegate {
    
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if navigationAction.request.url?.scheme == "blob" {
            decisionHandler(.download)
        } else {
            decisionHandler(.allow)
        }
    }
...

そうすると、新しいデリゲートメソッドであるwebView(_:navigationAction:didBecome:)が呼ばれるようになります。このメソッドにWKDownloadというWKWebView上でのダウンロードを担当するクラスのインスタンスが渡されるので、そのdelegateプロパティを指定してあげます。これにより、ダウンロード先の指定などができるようになります。

...
    func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
        download.delegate = self
    }
}

手順2. WKDownloadDelegateの実装

WKDownload用に新規追加されたWKDownloadDelegateプロトコルのdownload(_:decideDestinationUsing:suggestedFilename:completionHandler:)を以下のように実装します。suggestedFilenameによって提示されたファイル名からダウンロード先のURLを決定し、completionHandler()に渡してあげます。ここではtmpディレクトリ直下にそのまま保存するようにしてみました。

extension ViewController: WKDownloadDelegate {

    func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
        let url = FileManager.default.temporaryDirectory.appendingPathComponent(suggestedFilename)
        completionHandler(url)
    }
}

手順3. WKWebViewでダウンロード元のURLを開く

あとはこれまでのWKWebViewの使い方と同じです。delegateプロパティを指定し、ダウンロード元のURLをリクエストします。その後、Blobオブジェクトのダウンロードがはじまる操作(今回は「コンピューターに保存する」)をすると、ファイルのダウンロードがはじまり、指定したURLにファイルが保存されます。

import UIKit
import WebKit

class ViewController: UIViewController {
    
    @IBOutlet weak var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView.navigationDelegate = self
        
        let url = URL(string: "https://scratch.mit.edu/projects/editor/")!
        webView.load(URLRequest(url: url))
    }
}

おまけ1: Blobオブジェクト以外もダウンロードできるようにする

WKNavigationActionに新しくshouldPerformDownloadというプロパティが追加されており、それを使うとダウンロード対象のリクエストかどうかを判断することができます。blob:のときにはこれがtrueになるので、webView(_:decidePolicyFor:preferences:decisionHandler:)を以下のように実装することもできます。

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if navigationAction.shouldPerformDownload {
            decisionHandler(.download)
        } else {
            decisionHandler(.allow)
        }
    }

おまけ2: 任意のURLをダウンロードする

WKWebViewに追加されたstartDownload(using:completionHandler:)を使って、任意のURLをダウンロードすることもできます。ダウンロードの準備ができるとcompletionHandlerに指定したクロージャーが呼ばれるので、そこに渡されるWKDownloadのインスタンスにdelegateを指定してあげれば、WKDownloadDelegateプロトコルの各デリゲートメソッドが呼ばれるようになります。

webView.startDownload(using: URLRequest(url: URL(string: "https://scratch.mit.edu/")!)) { download in
    download.delegate = self
}