How to download a PDF via WKWebView but open it in QuickLookPreview?

Hello forum!


I have a special problem with my iOS App.


My setup is the following:

  • Swift 4 app with minimum target iOS 11
  • the app consists more or less of single view with a WKWebView which is connected to the client portal of a health insurance
  • the client portal provides a login (session cookies) and allows to download pdf documents when logged-in


I want the downloades pdf files to be opened in QuickLookPreviewController instead of WKWebView.

The customer need is that they want to be able to print or share their documents and I like to use the QuickLookPreview therefore, because it offers the best native feeling for this case.


Now to my problem:

I managed to download the documents to local storage (temp folder) and to open it in QuickLookPreviewController with a little trick:

The response of the download is intercepted (by using webView(..., decidePolicyFor ...)) and the download url is used to trigger a separate download (using URLSession.shared.dataTask(with: downloadUrl)) to iPhone storage, because QuickLookPreviewController needs a local file and cannot deal with the download url to the pdf.

In general this works - most of the time. Since I need to be in a logged-in state to download the pdf, I have to pass the authentication cookies from WKWebView (where I logged-in) to the shared cookie storage (which is used by the download task). This cookie sync between the two cookie storages is a known issue and I think I read all the related postings in the web, but couldn't get it working reliably.

In my debug logs I can see that my logic works in general, but sometimes the cookies aren't synchronized at all.


Here's my code. I appreciate every help 🙂


    
    
import UIKit  
import WebKit  
import QuickLook  
  
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, QLPreviewControllerDataSource {  
  
    @IBOutlet var webView: WKWebView!
    var documentPreviewController = QLPreviewController()
    var documentUrl = URL(fileURLWithPath: "")


    var webViewCookieStore: WKHTTPCookieStore!
    let webViewConfiguration = WKWebViewConfiguration()
    
...  
  
    override func viewDidLoad() {
        super.viewDidLoad()


        // link the appDelegate to be able to receive the deviceToken
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.viewController = self


        // initial configuration of custom JavaScripts
        webViewConfiguration.userContentController = userContentController


        // QuickLook document preview
        documentPreviewController.dataSource  = self
        
        webView = WKWebView(frame: CGRect.zero, configuration: webViewConfiguration)
        
        // see "The 2 delegates": https://samwize.com/2016/06/08/complete-guide-to-implementing-wkwebview/
        webView.uiDelegate = self
        webView.navigationDelegate = self


        view.addSubview(webView)


        webViewCookieStore = webView.configuration.websiteDataStore.httpCookieStore
        ...  
        load("https://clientportal.xyz"!)  
    }  
  
    private func load(_ url: URL) {  
        load(URLRequest(url:url))  
    }  
    private func load(_ req: URLRequest) {  
        var request = req  
        request.setValue(self.deviceToken, forHTTPHeaderField: "iosDeviceToken")  
        request.setValue(self.myVersion as? String, forHTTPHeaderField: "iosVersion")  
        request.setValue(self.myBuild as? String, forHTTPHeaderField: "iosBuild")  
        request.setValue(UIDevice.current.modelName, forHTTPHeaderField: "iosModelName")  
        debugPrintHeaderFields(of: request, withMessage: "Loading request")  
        webView.load(request)  
        debugPrint("Loaded request=\(request.url?.absoluteString ?? "n/a")")  
    }  
  
    /* 
     Intercept decision handling to be able to present documents in QuickLook preview 
     Needs to be intercepted here, because I need the suggestedFilename for download 
     */  
    func webView(_ webView: WKWebView,  
                 decidePolicyFor navigationResponse: WKNavigationResponse,  
                 decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {  
        let url = navigationResponse.response.url  
        if (openInDocumentPreview(url!)) {  
            let documentUrl = url?.appendingPathComponent(navigationResponse.response.suggestedFilename!)  
            loadAndDisplayDocumentFrom(url: documentUrl!)  
            decisionHandler(.cancel)  
        } else {  
            decisionHandler(.allow)  
        }  
    }  
  
    /* 
     Download the file from the given url and store it locally in the app's temp folder. 
     The stored file is then opened using QuickLook preview. 
     */  
    private func loadAndDisplayDocumentFrom(url downloadUrl : URL) {
        let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(downloadUrl.lastPathComponent)



        // getAllCookies needs to be called in main thread??? (https://medium.com/appssemble/wkwebview-and-wkcookiestore-in-ios-11-5b423e0829f8)
//??? needed?? DispatchQueue.main.async {        
            self.webViewCookieStore.getAllCookies { (cookies) in
            for cookie in cookies {
                if cookie.domain.range(of: "my.domain.xyz") != nil {
                    HTTPCookieStorage.shared.setCookie(cookie)
                    debugPrint("Sync cookie [\(cookie.domain)] \(cookie.name)=\(cookie.value)")
                } else {
                    debugPrint("Skip cookie [\(cookie.domain)] \(cookie.name)=\(cookie.value)")
                }
            }
            debugPrint("FINISHED COOKIE SYNC")
            
            debugPrint("Downloading document from url=\(downloadUrl.absoluteString)")
            URLSession.shared.dataTask(with: downloadUrl) { data, response, err in
                guard let data = data, err == nil else {
                    debugPrint("Error while downloading document from url=\(downloadUrl.absoluteString): \(err.debugDescription)")
                    return
                }
                
                if let httpResponse = response as? HTTPURLResponse {
                    debugPrint("Download http status=\(httpResponse.statusCode)")
                }




                // write the downloaded data to a temporary folder
                do {
                    try data.write(to: localFileURL, options: .atomic)   // atomic option overwrites it if needed
                    debugPrint("Stored document from url=\(downloadUrl.absoluteString) in folder=\(localFileURL.absoluteString)")
                    
                    DispatchQueue.main.async {
                        self.documentUrl = localFileURL
                        self.documentPreviewController.refreshCurrentPreviewItem()
                        self.present(self.documentPreviewController, animated: true, completion: nil)
                    }
                } catch {
                    debugPrint(error)
                    return
                }
            }.resume()
        }
    }
  
  
    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {  
        return documentUrl as QLPreviewItem  
    }  
  
    /* 
     We always have just one preview item 
     */  
    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {  
        return 1  
    }  
  
  
    /* 
     Checks if the given url points to a document provided by Vaadin FileDownloader and returns 'true' if yes 
     */  
    private func openInDocumentPreview(_ url : URL) -> Bool {  
        return url.absoluteString.contains("/APP/connector")  
    } 

P.S. I tried many solutions:

  • Reading and syncing the cookies in main thread (there are many hints in the web, that you have to acces cookies in this way) - does not work reliably
  • Reading the WKWebViews cookies via Javascript to avoid the main thread handling - but here I get too less information about the cookies (e.g. domain and path is missing)
  • I configured a WKProcessPool - didn't help


Most of the time the cookies are synchronized, but with the same implementation I get problems when trying it later on, e.g. when I uploaded the presumably working code to TestFlight. It even differs when using a solution in Simulator or on a real device.


The error is a downloaded file (HTTP status 200), but its size is less than 8 kB, because I wasn't allowed to download it it because of the missing synchronized authentication cookie.

Accepted Reply

Hi again


With external help, I was able to fix my problem.


The solution ist now to avoid the separate download task with URLSession.shared.dataTask and hence avoid the sync problem between the WKWebView cookie storage and the shared cookie storage.


The download ist now performed with JavaScript.

I'm able to detect and intercept the request in func webView(_ webView: WKWebView, decidePolicyFor navigationAction ...).

When such a request is detected, I cancel the decision handling and call the JavaScript part with the download.


The JavaScript is executed in the WKWebView and hence I have all the authorization cookies in place and the download is successfull.

JavaScript then returns the downloaded document as binary blob which is taken over by a JavaScript message handler in Swift.


Then I just have to write the blob to a file and open the file in QuickLook preview. Works like a charm!


Here's the code in case somebody struggles with the same problem:


class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, QLPreviewControllerDataSource, WKScriptMessageHandler {


    ...


    @IBOutlet var webView: WKWebView!
    var documentPreviewController = QLPreviewController()
    var documentUrl = URL(fileURLWithPath: "")
    var documentDownloadTask: URLSessionTask?


    override func viewDidLoad() {
        super.viewDidLoad()
        
        // initial configuration of custom JavaScripts
        webViewConfiguration.userContentController = userContentController
        webViewConfiguration.websiteDataStore = WKWebsiteDataStore.default()


        // init this view controller to receive JavaScript callbacks
        userContentController.add(self, name: "openDocument")
        userContentController.add(self, name: "jsError")


        // QuickLook document preview
        documentPreviewController.dataSource  = self
        
        webView = WKWebView(frame: CGRect.zero, configuration: webViewConfiguration)
        ...
    }
    
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url


  if openInDocumentPreview(url!) {
  decisionHandler(.cancel)
  executeDocumentDownloadScript(forAbsoluteUrl: url!.absoluteString)
  } else {
  decisionHandler(.allow)
  }


    }
    
    /*
     Handler method for JavaScript calls.
     Receive JavaScript message with downloaded document
     */
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        debugPrint("did receive message \(message.name)")


        if (message.name == "openDocument") {
            previewDocument(messageBody: message.body as! String)
        } else if (message.name == "jsError") {
            debugPrint(message.body as! String)
        }
    }
    
    /*
     Open downloaded document in QuickLook preview
     */
    private func previewDocument(messageBody: String) {
        // messageBody is in the format ;data:;base64,
        
        // split on the first ";", to reveal the filename
        let filenameSplits = messageBody.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)
        
        let filename = String(filenameSplits[0])
        
        // split the remaining part on the first ",", to reveal the base64 data
        let dataSplits = filenameSplits[1].split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
        
        let data = Data(base64Encoded: String(dataSplits[1]))
        
        if (data == nil) {
            debugPrint("Could not construct data from base64")
            return
        }
        
        // store the file on disk (.removingPercentEncoding removes possible URL encoded characters like "%20" for blank)
        let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename.removingPercentEncoding ?? filename)
        
        do {
            try data!.write(to: localFileURL);
        } catch {
            debugPrint(error)
            return
        }
        
        // and display it in QL
        DispatchQueue.main.async {
            self.documentUrl = localFileURL
            self.documentPreviewController.refreshCurrentPreviewItem()
            self.present(self.documentPreviewController, animated: true, completion: nil)
        }
    }
    
    /*
     Implementation for QLPreviewControllerDataSource
     */
    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        return documentUrl as QLPreviewItem
    }


    /*
     Implementation for QLPreviewControllerDataSource
     We always have just one preview item
     */
    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        return 1
    }


    /*
     Checks if the given url points to a document download url
     */
    private func openInDocumentPreview(_ url : URL) -> Bool {
    // this is specific for our application - can be everything in your application
        return url.absoluteString.contains("/APP/connector")
    }

    /*
     Intercept the download of documents in webView, trigger the download in JavaScript and pass the binary file to JavaScript handler in Swift code
     */
    private func executeDocumentDownloadScript(forAbsoluteUrl absoluteUrl : String) {
        // TODO: Add more supported mime-types for missing content-disposition headers
        webView.evaluateJavaScript("""
            (async function download() {
                const url = '\(absoluteUrl)';
                try {
                    // we use a second try block here to have more detailed error information
                    // because of the nature of JS the outer try-catch doesn't know anything where the error happended
                    let res;
                    try {
                        res = await fetch(url, {
                            credentials: 'include'
                        });
                    } catch (err) {
                        window.webkit.messageHandlers.jsError.postMessage(`fetch threw, error: ${err}, url: ${url}`);
                        return;
                    }
                    if (!res.ok) {
                        window.webkit.messageHandlers.jsError.postMessage(`Response status was not ok, status: ${res.status}, url: ${url}`);
                        return;
                    }
                    const contentDisp = res.headers.get('content-disposition');
                    if (contentDisp) {
                        const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i);
                        if (match) {
                            filename = match[3] || match[4];
                        } else {
                            // TODO: we could here guess the filename from the mime-type (e.g. unnamed.pdf for pdfs, or unnamed.tiff for tiffs)
                            window.webkit.messageHandlers.jsError.postMessage(`content-disposition header could not be matched against regex, content-disposition: ${contentDisp} url: ${url}`);
                        }
                    } else {
                        window.webkit.messageHandlers.jsError.postMessage(`content-disposition header missing, url: ${url}`);
                        return;
                    }
                    if (!filename) {
                        const contentType = res.headers.get('content-type');
                        if (contentType) {
                            if (contentType.indexOf('application/json') === 0) {
                                filename = 'unnamed.pdf';
                            } else if (contentType.indexOf('image/tiff') === 0) {
                                filename = 'unnamed.tiff';
                            }
                        }
                    }
                    if (!filename) {
                        window.webkit.messageHandlers.jsError.postMessage(`Could not determine filename from content-disposition nor content-type, content-dispositon: ${contentDispositon}, content-type: ${contentType}, url: ${url}`);
                    }
                    let data;
                    try {
                        data = await res.blob();
                    } catch (err) {
                        window.webkit.messageHandlers.jsError.postMessage(`res.blob() threw, error: ${err}, url: ${url}`);
                        return;
                    }
                    const fr = new FileReader();
                    fr.onload = () => {
                        window.webkit.messageHandlers.openDocument.postMessage(`${filename};${fr.result}`)
                    };
                    fr.addEventListener('error', (err) => {
                        window.webkit.messageHandlers.jsError.postMessage(`FileReader threw, error: ${err}`)
                    })
                    fr.readAsDataURL(data);
                } catch (err) {
                    // TODO: better log the error, currently only TypeError: Type error
                    window.webkit.messageHandlers.jsError.postMessage(`JSError while downloading document, url: ${url}, err: ${err}`)
                }
            })();
            // null is needed here as this eval returns the last statement and we can't return a promise
            null;
        """) { (result, err) in
            if (err != nil) {
                debugPrint("JS ERR: \(String(describing: err))")
            }
        }
    }
}
    
    
    
    
    
   

Replies

Hi again


With external help, I was able to fix my problem.


The solution ist now to avoid the separate download task with URLSession.shared.dataTask and hence avoid the sync problem between the WKWebView cookie storage and the shared cookie storage.


The download ist now performed with JavaScript.

I'm able to detect and intercept the request in func webView(_ webView: WKWebView, decidePolicyFor navigationAction ...).

When such a request is detected, I cancel the decision handling and call the JavaScript part with the download.


The JavaScript is executed in the WKWebView and hence I have all the authorization cookies in place and the download is successfull.

JavaScript then returns the downloaded document as binary blob which is taken over by a JavaScript message handler in Swift.


Then I just have to write the blob to a file and open the file in QuickLook preview. Works like a charm!


Here's the code in case somebody struggles with the same problem:


class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, QLPreviewControllerDataSource, WKScriptMessageHandler {


    ...


    @IBOutlet var webView: WKWebView!
    var documentPreviewController = QLPreviewController()
    var documentUrl = URL(fileURLWithPath: "")
    var documentDownloadTask: URLSessionTask?


    override func viewDidLoad() {
        super.viewDidLoad()
        
        // initial configuration of custom JavaScripts
        webViewConfiguration.userContentController = userContentController
        webViewConfiguration.websiteDataStore = WKWebsiteDataStore.default()


        // init this view controller to receive JavaScript callbacks
        userContentController.add(self, name: "openDocument")
        userContentController.add(self, name: "jsError")


        // QuickLook document preview
        documentPreviewController.dataSource  = self
        
        webView = WKWebView(frame: CGRect.zero, configuration: webViewConfiguration)
        ...
    }
    
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url


  if openInDocumentPreview(url!) {
  decisionHandler(.cancel)
  executeDocumentDownloadScript(forAbsoluteUrl: url!.absoluteString)
  } else {
  decisionHandler(.allow)
  }


    }
    
    /*
     Handler method for JavaScript calls.
     Receive JavaScript message with downloaded document
     */
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        debugPrint("did receive message \(message.name)")


        if (message.name == "openDocument") {
            previewDocument(messageBody: message.body as! String)
        } else if (message.name == "jsError") {
            debugPrint(message.body as! String)
        }
    }
    
    /*
     Open downloaded document in QuickLook preview
     */
    private func previewDocument(messageBody: String) {
        // messageBody is in the format ;data:;base64,
        
        // split on the first ";", to reveal the filename
        let filenameSplits = messageBody.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)
        
        let filename = String(filenameSplits[0])
        
        // split the remaining part on the first ",", to reveal the base64 data
        let dataSplits = filenameSplits[1].split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
        
        let data = Data(base64Encoded: String(dataSplits[1]))
        
        if (data == nil) {
            debugPrint("Could not construct data from base64")
            return
        }
        
        // store the file on disk (.removingPercentEncoding removes possible URL encoded characters like "%20" for blank)
        let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename.removingPercentEncoding ?? filename)
        
        do {
            try data!.write(to: localFileURL);
        } catch {
            debugPrint(error)
            return
        }
        
        // and display it in QL
        DispatchQueue.main.async {
            self.documentUrl = localFileURL
            self.documentPreviewController.refreshCurrentPreviewItem()
            self.present(self.documentPreviewController, animated: true, completion: nil)
        }
    }
    
    /*
     Implementation for QLPreviewControllerDataSource
     */
    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        return documentUrl as QLPreviewItem
    }


    /*
     Implementation for QLPreviewControllerDataSource
     We always have just one preview item
     */
    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        return 1
    }


    /*
     Checks if the given url points to a document download url
     */
    private func openInDocumentPreview(_ url : URL) -> Bool {
    // this is specific for our application - can be everything in your application
        return url.absoluteString.contains("/APP/connector")
    }

    /*
     Intercept the download of documents in webView, trigger the download in JavaScript and pass the binary file to JavaScript handler in Swift code
     */
    private func executeDocumentDownloadScript(forAbsoluteUrl absoluteUrl : String) {
        // TODO: Add more supported mime-types for missing content-disposition headers
        webView.evaluateJavaScript("""
            (async function download() {
                const url = '\(absoluteUrl)';
                try {
                    // we use a second try block here to have more detailed error information
                    // because of the nature of JS the outer try-catch doesn't know anything where the error happended
                    let res;
                    try {
                        res = await fetch(url, {
                            credentials: 'include'
                        });
                    } catch (err) {
                        window.webkit.messageHandlers.jsError.postMessage(`fetch threw, error: ${err}, url: ${url}`);
                        return;
                    }
                    if (!res.ok) {
                        window.webkit.messageHandlers.jsError.postMessage(`Response status was not ok, status: ${res.status}, url: ${url}`);
                        return;
                    }
                    const contentDisp = res.headers.get('content-disposition');
                    if (contentDisp) {
                        const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i);
                        if (match) {
                            filename = match[3] || match[4];
                        } else {
                            // TODO: we could here guess the filename from the mime-type (e.g. unnamed.pdf for pdfs, or unnamed.tiff for tiffs)
                            window.webkit.messageHandlers.jsError.postMessage(`content-disposition header could not be matched against regex, content-disposition: ${contentDisp} url: ${url}`);
                        }
                    } else {
                        window.webkit.messageHandlers.jsError.postMessage(`content-disposition header missing, url: ${url}`);
                        return;
                    }
                    if (!filename) {
                        const contentType = res.headers.get('content-type');
                        if (contentType) {
                            if (contentType.indexOf('application/json') === 0) {
                                filename = 'unnamed.pdf';
                            } else if (contentType.indexOf('image/tiff') === 0) {
                                filename = 'unnamed.tiff';
                            }
                        }
                    }
                    if (!filename) {
                        window.webkit.messageHandlers.jsError.postMessage(`Could not determine filename from content-disposition nor content-type, content-dispositon: ${contentDispositon}, content-type: ${contentType}, url: ${url}`);
                    }
                    let data;
                    try {
                        data = await res.blob();
                    } catch (err) {
                        window.webkit.messageHandlers.jsError.postMessage(`res.blob() threw, error: ${err}, url: ${url}`);
                        return;
                    }
                    const fr = new FileReader();
                    fr.onload = () => {
                        window.webkit.messageHandlers.openDocument.postMessage(`${filename};${fr.result}`)
                    };
                    fr.addEventListener('error', (err) => {
                        window.webkit.messageHandlers.jsError.postMessage(`FileReader threw, error: ${err}`)
                    })
                    fr.readAsDataURL(data);
                } catch (err) {
                    // TODO: better log the error, currently only TypeError: Type error
                    window.webkit.messageHandlers.jsError.postMessage(`JSError while downloading document, url: ${url}, err: ${err}`)
                }
            })();
            // null is needed here as this eval returns the last statement and we can't return a promise
            null;
        """) { (result, err) in
            if (err != nil) {
                debugPrint("JS ERR: \(String(describing: err))")
            }
        }
    }
}
    
    
    
    
    
   

Thank you so much! This helped me a lot today.

Hello !

I'm trying to implement your solution but I need to do this :
Code Block
const contentDisp = 'attachment; filename="document.pdf"';

I'm not getting the Content-Disposition header here, only content-length and content-type

I sniffed the network with Wireshark and could find the HTTP answer is all ok...

Code Block Frame 1843: 1341 bytes on wire (10728 bits), 1341 bytes captured (10728 bits) on interface en0, id 0
Internet Protocol Version 4, Src: 10.234.25.103, Dst: 10.234.24.155
Transmission Control Protocol, Src Port: 8080, Dst Port: 50584, Seq: 112813, Ack: 1277, Len: 1275
[80 Reassembled TCP Segments (113563 bytes): #1727(1448), #1728(1448), #1729(1448), #1730(1448), #1731(1448), #1732(952), #1733(1448), #1738(1448), #1739(1448), #1740(1448), #1741(1448), #1742(1448), #1743(1448), #1744(1448), #1745(1448), #]
Hypertext Transfer Protocol
HTTP/1.1 200 \r\n
Vary: Origin\r\n
Vary: Access-Control-Request-Method\r\n
Vary: Access-Control-Request-Headers\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Expose-Headers: Authorization\r\n
Content-Disposition: attachment; filename="3300050009001.pdf"\r\n
X-Content-Type-Options: nosniff\r\n
X-XSS-Protection: 1; mode=block\r\n
Cache-Control: no-cache, no-store, max-age=0, must-revalidate\r\n
Pragma: no-cache\r\n
Expires: 0\r\n
X-Frame-Options: DENY\r\n
Content-Type: application/pdf\r\n
Content-Length: 113042\r\n
Date: Wed, 12 Aug 2020 08:36:03 GMT\r\n
\r\n
[HTTP response 2/2]
[Time since request: 0.250664000 seconds]
[Prev request in frame: 1506]
[Prev response in frame: 1507]
[Request in frame: 1512]
[Request URI: http://10.234.25.103:8080/backend/documents/3300050009001.pdf]
File Data: 113042 bytes
Media Type

Do someone have an idea where the header would be filtered out ?
Hi,

Does this still work for you in iOS 14?
This approach was working for me in iOS 12 and 13.

It is broken and does not work in iOS 14.

In iOS 14 I see the following error in the logs:
Code Block
error: fetch threw, error: TypeError: The operation couldn’t be completed. (WebKitBlobResource error 1.), url: blob:https:yourbloblink


Has anyone else encountered this? Does anyone have working code or a solution for iOS 14?

I am unable to download and display PDF blobs in iOS 14 via WKWebView.
  •  denied by Cross-Origin Resource Sharing policy: Origin null is not allowed by Access-Control-Allow-Origin.Prompt me to cross domains after use

Add a Comment

I have a similar requirement of downloading & displaying PDF blobs via WKWebView and I'm able to achieve it with iOS 15 as we have WKDownloadDelegate available from iOS 15 onwards.

However, the above mentioned solution doesn't work for me with iOS 14 and I am unable to download & display PDF blobs in iOS 14 via WKWebView.

Any assistance on iOS 14 would be greatly appreciated.

  • @vivekdandage will you please share your code for download. I also have the same requirement but somehow my code is not working as expected. In webView: , decidePolicyFor navigationAction: delegate navigationAction.shouldPerformDownload always returns nil

Add a Comment