【Swift5】WKWebViewで動的に生成されたファイルをダウンロードする


はじめに

WKWebView内のJavaScriptで動的に生成されたファイルを端末に保存するのに苦労しました。

静的なファイル、例えば「〇〇.png」とかなら、既にファイルのURLが分かっているので簡単にダウンロードできます。

しかし、生成されたファイルがBlobやDataURI(Base64)などの形式で出力されていると、それ自体はファイル名や拡張子を保持していないので、Swift側で名前をつける必要が出てきます。(PCのブラウザだと勝手にこれらの保存処理をやってくれます)

ここでは、JSONファイルを文字列として送信することで、生成されたファイルとファイル名を同時に送る方法を書いておこうと思います。

追記

先日一般公開されたiOS14.5では、WKWebView標準の機能でBlobファイルがダウンロードできるようになったそうです。

iOS14.5以降をターゲットにしたアプリの場合は、こちらを利用する方がいいかと思います。


Blobとは

データベース中に格納される、画像や音声、映像といったバイナリ型の可変長データ・オブジェクトのこと。

 一般的にデータベース・システムでは、数値や文字列など、検索可能なデータを格納することが多い。だが最近のデータベース・システムでは、画像や音声、映像といった、比較的サイズが大きく、可変長のバイナリ・データも格納することができる。これをBLOBという。ただしバイナリ・データの内部形式はそれを使用するアプリケーションごとに固有であり、データベース・システムがこれらの内容を解釈することはできない。そのためBLOBデータの内容を検索の対象とすることはできず、同一レコード中に格納されたほかのキーを使って検索したり、アクセスしたりする必要がある。

(引用:Insider’s Computer Dictionary:BLOB とは?

データベースに保存しておくためのデータ形式の一つだそうです。

アプリケーション側がいじるためのものなので、データベースは中身を知らずに保存してるだけ、ということみたいです。

DataURIとは

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAuoAAALqCAYAAABjZ3(長すぎるので省略)FJLLfWerf8P1j285JOyQ0AAAAAASUVORK5CYII=

のようなデータです。

data:から始まるURLは「DataURI」と呼ばれ、Swiftでもダウンロード元のURLとして使用できます。

普通のURLと異なる点は、後ろの文字の羅列(Base64文字列)がファイルの中身を全て表しているということです。

何を使うか

userContentController

これを使うと、JavaScriptとSwiftで双方向に通信を行えます。 今回は、JS側からSwift側に文字列を送信します。

JSON

userContentControllerでDataURIを送っただけではファイル名も拡張子もわからないので、JSONを使って二つの文字列を送れるようにします。

JS側の手順

nameblobを送りたい場合のサンプルです。

blob形式からDataURIに変換してSwift側に送信しています。

const name = "sample.png";
const blob = "〇〇";

const reader = new FileReader();
reader.onloadend = function () {
    let data = {fileName: name, path: this.result};
    let json = JSON.stringify(data);
    window.webkit.messageHandlers.handler.postMessage(json);
};
reader.readAsDataURL(blob);

Swift側の手順

まず、jsonを入れるための構造体を用意します。

struct File: Decodable {
    let fileName: String
    let path: String
}

次に、viewDidLoadあたりでuserContentControllerを追加しておきます。

webView.configuration.userContentController.add(self, name: "handler")
extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.name {
        case "handler":
            //print("\(message.body)")
            saveData(message.body)
        default:
            print("message error")
        }
    }
    
    func saveData(_ json:Any) {
        if let jsonStr = json as? String {
            let jsonData = jsonStr.data(using: .utf8)
            let decoder = JSONDecoder()
            let file = try! decoder.decode(File.self, from: jsonData!)
            print(file.fileName)
            
            let fileUrl = URL(string: file.path)!
            if let data = try? Data(contentsOf: fileUrl) {
                
                let fileManager = FileManager.default
                        
                let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
                let targetURL = URL(fileURLWithPath: documents).appendingPathComponent(file.fileName)
                
                fileManager.createFile(atPath: targetURL.path,
                                           contents: data, attributes: nil)
            }else{
                print("couldn't get data")
            }
        }
    }
}

これでdocumentsフォルダにファイルをダウンロードできました。

Comments

Show Comments