How to download files in wkwebview - swift

Anybody please tell me how to download files in iOS wkwebview. I have created an iOS webview app. In the page I have loaded It has several download options but when I click download nothing happened.
Note: I dont want to create extra buttons to download

Since macOS 11.3 and iOS 14.5 we have finally an API to deal with downloads.
But at the time of writing this (June, 2021), documentation is still quite limited: WKDownloadDelegate
1. WKNavigationDelegate
1.1
Add a WKNavigationDelegate to your WKWebView.navigationDelegate
1.2
On your WKNavigationDelegate implement:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: #escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
if navigationAction.shouldPerformDownload {
decisionHandler(.download, preferences)
} else {
decisionHandler(.allow, preferences)
}
}
This will get called when clicking any link.
navigationAction.shouldPerformDownload will be true when the WKWebView detects the link is meant to download a file.
1.3
Also on your WKNavigationDelegate implement:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if navigationResponse.canShowMIMEType {
decisionHandler(.allow)
} else {
decisionHandler(.download)
}
}
This will get called if you answered decisionHandler(.allow, preferences) on the first method, meaning the WKWebView didn't recognise the link as a download, and will try to display it.
navigationResponse.canShowMIMEType will be false if the WKWebView realises it can't display the content.
2. WKDownloadDelegate
2.1
Create a WKDownloadDelegate
2.2
In your WKWebView implement:
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
download.delegate = // your `WKDownloadDelegate`
}
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
download.delegate = // your `WKDownloadDelegate`
}
One of these methods will be called when you answer .download to any of the methods described on section 1.. The first will be called if it was the first method and second if it was the second method.
You need to assign a delegate to each download, but it can be the same delegate for all of them.
2.3
In your WKDownloadDelegate implement:
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: #escaping (URL?) -> Void) {
let url = // the URL where you want to save the file, optionally appending `suggestedFileName`
completionHandler(url)
}
This will get called when WKWebView is ready to start a download, but needs a destination URL.
2.4
Optionally, also in your WKDownloadDelegate implement:
func downloadDidFinish(_ download: WKDownload) {
}
This will get called when the download finishes.
Final notes
Remember both delegates are not retained by the WKWebView, so you need to retain them yourself.
There are a few more methods on WKDownloadDelegate useful for dealing with errors, check the documentation for more details (link provided above).
Important to remember this is only supported on macOS 11.3 and iOS 14.5.
As mentioned before, documentation is still scarce, I just finded how to make this work by trial an error, any feedback appreciated.

You can also use JavaScript to download your file, as Sayooj's link implys.
Of course, you will handle the file downloaded code yourself.
With func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) { , you get the file url to download.
Then download it with JS.
the JS call a downloaded method if success, you will be notified with public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { ,
Then you can handle your downloaded file
It is a little complicated. Use JavaScript to download file, use WKScriptMessageHandler to communicate between native Swift and JavaScript.
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler {
var webView: WKWebView!
let webViewConfiguration = WKWebViewConfiguration()
override func viewDidLoad() {
super.viewDidLoad()
// init this view controller to receive JavaScript callbacks
webViewConfiguration.userContentController.add(self, name: "openDocument")
webViewConfiguration.userContentController.add(self, name: "jsError")
webView = WKWebView(frame: yourFrame, configuration: webViewConfiguration)
}
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url
decisionHandler(.cancel)
executeDocumentDownloadScript(forAbsoluteUrl: url!.absoluteString)
}
/*
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") {
handleDocument(messageBody: message.body as! String)
} else if (message.name == "jsError") {
debugPrint(message.body as! String)
}
}
/*
Open downloaded document in QuickLook preview
*/
private func handleDocument(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 {
// localFileURL
// now you have your file
}
}
/*
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))")
}
}
}
}

As Sayooj's link implys:
You have to deal with the download business yourself
After you have download task in WKWebView, you can get the file url to download from method func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
Then you initiate a download task to download the file, URLSession is an option
you can handle the file after downloaded. And the link above shows how to preview your downloaded file with QLPreviewController
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
var webView: WKWebView!
var webViewCookieStore: WKHTTPCookieStore!
let webViewConfiguration = WKWebViewConfiguration()
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: yourFrame, configuration: webViewConfiguration)
webView.uiDelegate = self
webView.navigationDelegate = self
view.addSubview(webView)
webView.load(URLRequest(url: yourUrlString))
}
/*
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
let documentUrl = url?.appendingPathComponent(navigationResponse.response.suggestedFilename!)
loadAndDisplayDocumentFrom(url: documentUrl!)
decisionHandler(.cancel)
}
/*
Download the file from the given url and store it locally in the app's temp folder.
*/
private func loadAndDisplayDocumentFrom(url downloadUrl : URL) {
let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(downloadUrl.lastPathComponent)
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 {
// localFileURL
// here is where your file
}
} catch {
debugPrint(error)
return
}
}.resume()
}
}

I had a similar situation and the answers here helped me on the way but it was not straight forward to just get up and running in SwiftUI. so here is the code snippet to handle all the download for you.
import SwiftUI
import WebKit
import OSLog
#available(iOS 14.5, *)
struct WebView: UIViewRepresentable {
lazy var logger = Logger()
#Binding var editorDownloadUrl: URL?
var downloadUrl = URL(fileURLWithPath: "")
func makeCoordinator() -> WebViewCoordinator{
return WebViewCoordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
webView.navigationDelegate = context.coordinator // very important to add this line.
guard let url = URL(string: "https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/") else { return }
let request = URLRequest(url: url)
webView.load(request)
}
}
// MARK: - WKNavigationDelegate
#available(iOS 14.5, *)
class WebViewCoordinator: NSObject, WKNavigationDelegate {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: #escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
return navigationAction.shouldPerformDownload ? decisionHandler(.download, preferences) : decisionHandler(.allow, preferences)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
navigationResponse.canShowMIMEType ? decisionHandler(.allow) : decisionHandler(.download)
}
}
// MARK: - WKDownloadDelegate
#available(iOS 14.5, *)
extension WebViewCoordinator: WKDownloadDelegate {
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
download.delegate = self
}
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: #escaping (URL?) -> Void) {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileUrl = documentDirectory.appendingPathComponent("\(suggestedFilename)", isDirectory: false)
parent.downloadUrl = fileUrl
completionHandler(fileUrl)
}
// MARK: - Optional
func downloadDidFinish(_ download: WKDownload) {
parent.editorDownloadUrl = parent.downloadUrl
}
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
parent.logger.error("\(error.localizedDescription)")
// you can add code here to continue the download in case there was a failure.
}
}

Related

SwiftUI: Cannot open window.open() javascript call on page in wkwebview with ui delegate createWebViewWithConfiguration

I have gone through a lot of different StackOverflow threads & Apple Developer pages to figure out what is going on here. I believe I am so close.
I have a WebView that runs on a site for a bit. Eventually the user can click a link that would open a new tab. It is opened via a window.open() call in JavaScript in the HTML.
However, on SwiftUI nothing runs, and I have set-up what should be the proper UIDelegate. This function never gets entered as I step through the call to WebView.
My question is: How can I make sure the UIDelegate is properly set? Where does this occur? Do I have the right spot?
My implementation (shortened):
struct WebView: UIViewRepresentable {
var url: URL
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
// Enable javascript in WKWebView to interact with the web app
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
preferences.javaScriptCanOpenWindowsAutomatically = true
let configuration = WKWebViewConfiguration()
// Here "iOSNative" is our interface name that we pushed to the website that is being loaded
// configuration.userContentController.add(self.makeCoordinator(), name: "iOSNative")
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
// webView.navigationDelegate = context.coordinator
// webView.uiDelegate = context.coordinator
// webView.allowsBackForwardNavigationGestures = true
// webView.scrollView.isScrollEnabled = true
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.uiDelegate = context.coordinator
webView.navigationDelegate = context.coordinator
webView.load(request)
}
class Coordinator : NSObject, WKUIDelegate, WKNavigationDelegate {
var parent: WebView
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ uiWebView: WebView) {
// uiWebView.uiDelegate = self
self.parent = uiWebView
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
if navigationAction.targetFrame == nil {
if let url = navigationAction.request.url {
UIApplication.shared.open(url)
decisionHandler(.allow)
return
}
}
func webView(webView: WKWebView!, createWebViewWith configuration: WKWebViewConfiguration!, forNavigationAction navigationAction: WKNavigationAction!, windowFeatures: WKWindowFeatures!) -> WKWebView! {
// if navigationAction.targetFrame == nil {
// UIApplication.shared.open(navigationAction.request.url!)
// }
// return nil
if (!(navigationAction.targetFrame?.isMainFrame ?? false)) {
webView.load(navigationAction.request)
}
return nil
}
func webView(webView: WKWebView!, createWebViewWithConfiguration configuration: WKWebViewConfiguration!, forNavigationAction navigationAction: WKNavigationAction!, windowFeatures: WKWindowFeatures!) -> WKWebView! {
if navigationAction.targetFrame == nil {
UIApplication.shared.open(navigationAction.request.url!)
}
return nil
}
}
}
My thoughts:
Am I assigning uiDelegate & navigationDelegate in the right spots? It doesn't seem that my createWebViewWith delegate ever gets called. The app opens HTML links in tags with target = "_blank", it's just this one that is called via window.open(x) in javascript.
My resources:
WKWebView and window.open
Window.open() is not working in WKWebView
https://developer.apple.com/forums/thread/664267
https://developer.apple.com/forums/thread/68427

didFailProvisionalLoadForFrame when trying to download file with wkwebview

I have a WKWebView and on my page i have a button, that generates a pdf that is then offered for download., so when i click it in any browser it will immediatly start the download. I want that for wkwebview, too. I did some research and implemented the following:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if !navigationResponse.canShowMIMEType {
decisionHandler(.download)
} else {
decisionHandler(.allow)
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: #escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
if navigationAction.shouldPerformDownload {
decisionHandler(.download, preferences)
} else {
decisionHandler(.allow, preferences)
}
}
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
download.delegate = self
}
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
download.delegate = self
}
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: #escaping (URL?) -> Void) {
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let url = dir.appendingPathComponent(suggestedFilename)
completionHandler(url)
}
}
but when i click it, nothing happens and the console shows the following error:
WebPageProxy::didFailProvisionalLoadForFrame: frameID=3, domain=WebKitErrorDomain, code=102, isMainFrame=1
Try adding the following delegate function:
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
print("webkit download fail: \(error.localizedDescription)")
}
If you get the message "The operation couldn’t be completed. (NSURLErrorDomain error -3000.)", the problem is most likely that you have an existing file with the same name in your download destination directory. More info can be found here.
You may also want to add the following delegate function to confirm if your file is actually downloaded:
func downloadDidFinish(_ download: WKDownload) {
print("webkit download finished!")
}

WKWebView not calling navigation delegate methods

I'm trying to export an html file as a pdf using the answer here which uses a WKWebView to load an html string and then draw a PDF from that. I also need it to work on Catalyst, which is why I'm using this approach.
What's happening is that the WKWebView is never calling it's navigationDelegate methods. I've put a bunch of print() statements in a bunch of delegate methods but the only output I get is:
::::: SET WEBVIEW
Loaded
I can't figure out what is making the WKWebView not call it's delegate methods. I've also tried loading a URL like https://google.com and it fails the same way. I've even observed loading progress from the webview, and it is loading the content, just not calling the delegate.
I'd appreciate any help, I need this to work.
class ReportsRenderer: NSObject {
var webView: WKWebView? = nil {
didSet {
print("::::: SET WEBVIEW")
}
}
var completion: PDFCompletion!
typealias PDFCompletion = (Result<NSData, Error>) -> Void
func exportPDF(html: String, completion: #escaping PDFCompletion) throws {
self.completion = completion
webView = WKWebView()
webView?.navigationDelegate = self
let baseURL = URL(fileURLWithPath: NSTemporaryDirectory())
webView?.loadHTMLString(html, baseURL: baseURL)
print("Loaded")
}
func createPDF(_ formatter: UIViewPrintFormatter) {
print("Creating PDF")
let printPageRenderer = UIPrintPageRenderer()
printPageRenderer.addPrintFormatter(formatter, startingAtPageAt: 0)
let paperRect = CGRect(x: 0, y: 0, width: 595.2, height: 841.8)
let padding: CGFloat = 20
let printableRect = paperRect.insetBy(dx: padding, dy: padding)
printPageRenderer.setValue(printableRect, forKey: "printableRect")
printPageRenderer.setValue(paperRect, forKey: "paperRect")
printPageRenderer.footerHeight = 70
printPageRenderer.headerHeight = 20
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)
for i in 0..<printPageRenderer.numberOfPages {
UIGraphicsBeginPDFPage();
printPageRenderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
}
UIGraphicsEndPDFContext();
self.completion?(.success(pdfData))
}
}
extension ReportsRenderer: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("::::: WEBVIEW NAVIGATED")
let viewPrintFormatter = webView.viewPrintFormatter()
createPDF(viewPrintFormatter)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("::::: WEBVIEW ERROR")
print(error.localizedDescription)
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("::::: WEBVIEW DIDCOMMIT NAVIGATION")
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("::::: WEBVIEW didStartProvisionalNavigation")
}
}
I'm calling it in my ViewController like:
do {
try ReportsRenderer().exportPDF(html: string) { (result) in
switch result {
case .success(let pdfData):
// ---- This is never called -----
print("Made the pdf data!")
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let pdfFilename = paths[0].appendingPathComponent("\(UUID().uuidString).pdf")
pdfData.write(to: pdfFilename, atomically: true)
case .failure(let error):
// ---- This is also never called -----
print("::::: Error")
print(error.localizedDescription)
}
}
} catch {
// There isn't an error here either
print("::::: Error")
print(error)
}
You need to create an instance of ReportsRenderer in ViewController to make it work. When you create an instance in ViewController it lives as long as the ViewController lives. In your case, it was getting deinitialized immediately.
class ViewController: UIViewController {
let reportsRenderer = ReportsRenderer()
// then...
do {
try reportsRenderer.exportPDF(html: string) { (result) in
//...
}

NSInternalInconsistencyException Completion handler passed to webView:decidePolicyForNavigationAction:decisionHandle was not called

I'm running into an issue with one of our iOS app tests. Any help to solve is much appreciated!
Test code:
func testOpenExternalBrowser_for_valid_urls() {
// Given
let vc = MockDashboardViewController()
vc.openInternalBrowserCalled = false
vc.openExternalBrowserCalled = false
vc.webview = WKWebView()
vc.webview.navigationDelegate = vc
// When
let url = URL.init(string: "https://example.com/restOfUrl")
XCTAssertTrue(url!.absoluteString.contains("https://example.com"), "initial url string is wrong")
vc.webview.load(URLRequest(url: url!));
waitSeconds(duration: 2)
// Then
XCTAssertFalse(vc.openExternalBrowserCalled, "openExternalBrowserCalled value is wrong")
XCTAssertTrue(vc.openInternalBrowserCalled, "openInternalBrowserCalled value is wrong")
}
This gives me the error:
error: -[Tests.ViewControllerTests testOpenExternalBrowser_for_valid_urls] : failed: caught
"NSInternalInconsistencyException", "Completion handler passed to
-[ViewController webView:decidePolicyForNavigationAction:decisionHandler:] was not
called"
This is the relevant WKWebKit ViewController code:
// MARK: WKNavigationDelegate methods
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url, let urlScheme = url.scheme, let urlHost = url.host else {
//if we can't convert the URL, deny the action
decisionHandler(WKNavigationActionPolicy.cancel);
return;
}
var isPortValid = false;
if let urlPort = url.port {
isPortValid = urlPort == HostDefinitions.PORT;
} else {
isPortValid = urlScheme == DashboardConstants.HTTPS;
}
if(isPortValid && urlScheme == HostDefinitions.SCHEME && urlHost.compare(HostDefinitions.HOST) == ComparisonResult.orderedSame) {
openInternalBrowser(url: url, decisionHandler: decisionHandler)
return;
}
decisionHandler(WKNavigationActionPolicy.cancel);
openExternalBrowser(url: url)
}
func openInternalBrowser(url: URL, decisionHandler:#escaping (WKNavigationActionPolicy) -> Void) {
if(url.path == PathDefinitions.Login) {
//the user has logged out or visited the login page. We will now de-auth them and direct them back to the login page
OperationQueue.main.addOperation {
AuthManager.logout()
_ = self.navigationController?.popToRootViewController(animated: true);
}
decisionHandler(WKNavigationActionPolicy.cancel);
} else {
decisionHandler(WKNavigationActionPolicy.allow);
}
}
func openExternalBrowser(url: URL) {
AppManager.openExternalBrowser(url: url)
}
Any ideas? I've tried adding a return statement and adding the decision handler to the function openExternalBrowser but neither work. The app functions properly (opening external URLs in Safari) but the test fails.
Thank you!
Edit:
Here's the MockDashboardViewController code:
class MockDashboardViewController: DashboardViewController {
var openInternalBrowserCalled = false
var openExternalBrowserCalled = false
var presentNotificationCalled = false
var webviewURLString = ""
override func openInternalBrowser(url: URL, decisionHandler:#escaping (WKNavigationActionPolicy) -> Void) {
openInternalBrowserCalled = true
}
override func openExternalBrowser(url: URL) {
openExternalBrowserCalled = true
}
override func presentNotification(notification: DashboardNotification, showAlertView: Bool) {
presentNotificationCalled = true
}
}
The error message is correct. You are saying:
let vc = MockDashboardViewController()
vc.webview.navigationDelegate = vc
So the web view's navigation is not your "relevant WKWebKit ViewController", which I presume is a DashboardViewController, but a subclass of DashboardViewController, namely MockDashboardViewController. And it says:
override func openInternalBrowser(url: URL, decisionHandler:#escaping (WKNavigationActionPolicy) -> Void) {
openInternalBrowserCalled = true
}
Okay, so what happens when decidePolicyFor is called? It isn't implemented in MockDashboardViewController, so DashboardViewController's implementation is called:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if(isPortValid && urlScheme == HostDefinitions.SCHEME && urlHost.compare(HostDefinitions.HOST) == ComparisonResult.orderedSame) {
openInternalBrowser(url: url, decisionHandler: decisionHandler)
return;
}
}
And that's all that happens along that path of execution. Now, where in all of that is the decisionHandler called? Nowhere. The runtime is perfectly correct.
It's true that decisionHandler would have been called in DashboardViewController's implementation of openInternalBrowser(url:decisionHandler:). That is why the app runs fine when you run the app (not testing). But in your testing subclass, you overrode that — and you didn't call super, so you threw away those calls to the decisionHandler.

Open UIWebView Links In Safari

I have an iOS app using Swift 3. I have a uiwebview in this app. Whenever a user clicks on a link in the webview, it opens up inside the webview. I need it to where the links open up inside the Safari app on the iPhone. Here's the code:
class VideosViewController: UIViewController {
#IBOutlet var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
var URL = NSURL(string: "http://example.com")
webView.loadRequest(NSURLRequest(url: URL! as URL) as URLRequest)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Can you please give me some code that I can enter? I'm new at coding, so thanks a lot.
UPDATE: Thanks to an answer I got some code, but the links are still not opening in Safari. Here's the code so far...
class VideosViewController : UIViewController, UIWebViewDelegate {
#IBOutlet weak var webView : UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "http://example.com") else { return }
let request = URLRequest(url: url)
webView.loadRequest(request)
}
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == .linkClicked {
guard let url = request.url else { return true }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return false
}
return true
}
}
Using UIWebView:
Make sure your view controller conforms to UIWebViewDelegate and then implement this method in the controller:
class VideosViewController : UIViewController, UIWebViewDelegate {
#IBOutlet weak var webView : UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "http://example.com") else { return }
webView.delegate = self
let request = URLRequest(url: url)
webView.loadRequest(request)
}
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == .linkClicked {
guard let url = request.url else { return true }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return false
}
return true
}
}
Using WKWebView:
Use a WKWebView instead of a UIWebView and make sure your controller conforms to the WKNavigationDelegate protocol. So your implementation would look something like the following:
class VideosViewController : UIViewController, WKNavigationDelegate {
var webView : WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "http://example.com") else { return }
webView = WKWebView(frame: self.view.frame)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.isUserInteractionEnabled = true
webView.navigationDelegate = self
self.view.addSubview(self.webView)
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Check if a link was clicked
if navigationAction.navigationType == .linkActivated {
// Verify the url
guard let url = navigationAction.request.url else { return }
let shared = UIApplication.shared
// Check if opening in Safari is allowd
if shared.canOpenURL(url) {
// Ask the user if they would like to open link in Safari
let alert = UIAlertController(title: "Open link in Safari?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (alert: UIAlertAction) -> Void in
// User wants to open in Safari
shared.open(url, options: [:], completionHandler: nil)
}))
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
decisionHandler(.cancel)
}
decisionHandler(.allow)
}
}
This way, when a user clicks a link in the web view, they will be prompted with an alert that will ask them whether or not they would like to open it in Safari, and if it is allowed, Safari will launch and the link will be opened.