loading...

Download files in a WKWebView

gualtierofr profile image Gualtiero Frigerio ・4 min read

As you know if you follow me I have many hybrid apps, with native code and web content. Recently I encountered a problem: I needed to show a web page and download a file from it. As most developer do these days, I looked for this very post title on Google, and I couldn’t find a simple and straight answer, so this blog post is for the future self, as usual, but even for a guy like you who’s looking for the same answer.
I’ll show you how to detect a mime type you may be interested into (PDF, Excel etc.) and instead of letting the WKWebView open it save the file with a URLSession and then offer the user the ability to move the file with a share sheet. He may open it in another app, or store it into the File app.

The sample project

As usual you can find all the code in this article on GitHub
It is a simple ViewController that instantiates the class WKWebViewDownloadHelper. The class is the navigationDelegate of the WKWebView and is responsible for detecting the correct mimeType, download the file and call its delegate with the path of the file.

MIME Type

When browsing the web in our WKWebView we may want to download some kind of files. A web view is perfectly capable of displaying a PDF document, but although it supports a basic view of an Excel file you may want to open it with Numbers or MS Excel.
How do we know the URL we are opening contains an Excel document? By looking at the MIME type.
Our class is the navigationDelegate for the WKWebView and we have two methods called each time the page wants to redirect to a new URL.
The first one is decidePolicyFor navigationAction, and the second is decidePolicyFor navigationResponse. The latter is called when we received the first response from the server, we can still tell the web view to avoid opening the URL and we need this method so we can parse the response to read the HTTP headers.

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        if let mimeType = navigationResponse.response.mimeType {
...
}

mimeType is a string, representing, guess what, the MIME type.
I found out there are a few MIME types for Excel, for example application/x-ms-excel application/vnd.ms-excel application/x-ms-excel
In my example I wrote a simple function to find a portion of text, so you can configure excel as your MIME type and all the types above will match.
There is another HTTP header we can read in order to find out the file name. The header is “Content-Disposition” and when we download a file we may find the string “attachment; filename=test.xls”.

if isMimeTypeConfigured(mimeType) {
    if let url = navigationResponse.response.url {
        let fileName = getFileNameFromResponse(navigationResponse.response)
        downloadData(fromURL: url, fileName: fileName) { success, destinationURL in
           if success, let destinationURL = destinationURL {
               self.delegate.fileDownloadedAtURL(url: destinationURL)
            }
        }
        decisionHandler(.cancel)
        return
    }
}

private func getFileNameFromResponse(_ response:URLResponse) -> String {
    if let httpResponse = response as? HTTPURLResponse {
        let headers = httpResponse.allHeaderFields
        if let disposition = headers["Content-Disposition"] as? String {
            let components = disposition.components(separatedBy: " ")
            if components.count > 1 {
                let innerComponents = components[1].components(separatedBy: "=")
                if innerComponents.count > 1 {
                    if innerComponents[0].contains("filename") {
                        return innerComponents[1]
                    }
                }
            }
        }
    }
    return "default"
}

Download the file

Once we have the file name, we can start downloading the file.
The site we’re visiting, may require authentication. The WKWebViews is able to manage cookies, you can see for yourself that if you open the site with your app a second time you don’t have to login again, as it happens in Safari. We cannot download the file with the WKWebView directly though, we do need to open a URLSession and if there is authentication, we need to pass it to the session, otherwise the download will fail.

private func downloadData(fromURL url:URL,
                          fileName:String,
                          completion:@escaping (Bool, URL?) -> Void) {
    webView.configuration.websiteDataStore.httpCookieStore.getAllCookies() { cookies in
        let session = URLSession.shared
        session.configuration.httpCookieStorage?.setCookies(cookies, for: url, mainDocumentURL: nil)
        let task = session.downloadTask(with: url) { localURL, urlResponse, error in
            if let localURL = localURL {
                let destinationURL = self.moveDownloadedFile(url: localURL, fileName: fileName)
                completion(true, destinationURL)
            }
            else {
                completion(false, nil)
            }
        }

        task.resume()
    }
}

The first line of this function copies the cookies stored into the web view. Once we have the cookies, we can add them to the URLSession we’re about to use to download the file.
With the cookies set, we can use a downloadTask to download a file, and when we’re done we can call the callback which will notify the delegate about the new file. As you can see, I’m using the file name specified by the caller to move the downloaded file from URLSession to a new destination inside my app. I’m using the temporary directory but you can use Cache or Document as well.

Once the file is downloaded we may give control to the user with a share sheet, so he can open it in Excel or save it in the File app

func fileDownloadedAtURL(url: URL) {
    DispatchQueue.main.async {
        let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
        activityVC.popoverPresentationController?.sourceView = self.view
        activityVC.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem
        self.present(activityVC, animated: true, completion: nil)
    }
}

That’s it. Hope you found this quick tutorial useful. Happy coding 🙂

Original post

Posted on by:

gualtierofr profile

Gualtiero Frigerio

@gualtierofr

Senior iOS developer at Epress spa

Discussion

pic
Editor guide