How to open external url using WKWEBVIEW - swift

I have an app which for example the domain is hello.example.com, but when the user click on a link which is example.com/example it has to open it as external link. I am using swift 5 and WKwebview
I have this code, but I need that when the url contains hello, the url opens in the webview otherwise open safari, but it doesn't work. I am looking for url contains something like that but i did not find something useful for my case.
I have this code , but obviously does not do what I need.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
if url.host != "example.com" {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}

You were on the right track.
Replace if url.host != "example.com" with if url.path.contains(…) and it should work.
Using url.path.contains(…) we can check the existence of a given string.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
let trigger = "example" // or "hello"
if url.path.contains(trigger) {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}

Related

Swift WebView not loading some redirections

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url,
let scheme = url.scheme else {
decisionHandler(.cancel)
return
}
//UIApplication.shared.open(url, options: [:] <- works but i dont want to open it in safari
//webView.load(URLRequest(url: url)) <- dont works
decisionHandler(.allow) <- dont works
}
Not loading this website whit neither the load or decisionHandler method: (it loads the rest of the links, but not this ad)
https://googleads.g.doubleclick.net/pcs/click?xai=AKAOjsulcITSmZ0z5pq-Fm4gRcby0MY3XDGdBQ9P0VcUSq5vRrRY5oquSebdiCSkVQsI3C-5KypcWh_RVVN9I62STn_vbYr6hA6UcYTpZEfw0Hm1swqBGpmD3ZJDq1QCoC_XOkNMLGXHJwm4Jk9Zjw0l9K-HsGUbzZmGRusq1qZ-yJEXVMWtDF7oBM3uayRqQ4QCsJPJHCgzl9dULq-cAQvnYG4mlM3_Lo-EenB1UXciUMthZX-qzjD0ioVHbezNjN94Ona5UdkmOg4v3ipF3RVFXO7YyPjsblSkUOI5K5VgtHi3KObSDZDtg3g2x0AAASgU8Q&sai=AMfl-YSar3L61Tu_2cYOdew53xYAKFUkSe0nXaSrK_cbcVfon6D6hUTQ7tQmArm_V2o3VspTdDNjXQ7aSCVdw1XsGfAzrbheRzyS2lgFDbfsTrTR&sig=Cg0ArKJSzBZk-HtVwUYF&fbs_aeid=%5Bgw_fbsaeid%5D&adurl=https://www.iberdrola.es/servicios/equipos/smart-solar/oferta-energia-solar-unifamiliar-comunidades&nm=1&nx=225&ny=-37&mb=1&clkt=2
And this one not even calling the func decidePolicyFor navigationAction:
https://www.addthis.com/bookmark.php?v=300&winname=addthis&pub=ra-62320da012f4f327&source=tbx-300&lng=es&s=whatsapp&wid=i1gy&url=https%3A%2F%2Fwww.tribunavalladolid.com%2F&title=Tribuna%20de%20Valladolid.%20Noticias%20Castilla%20y%20Le%C3%B3n.%20Noticias%20Valladolid&ate=AT-ra-62320da012f4f327/-/-/62ff3759d4f4d6bb/8&frommenu=1&ips=1&uid=62cfeb788b64c651&description=Noticias%20sobre%20Valladolid%20y%20su%20provincia.%20Deportes%2C%20sucesos%2C%20cultura%2C%20econom%C3%ADa%2C%20empresas%2C%20en%20tu%20peri%C3%B3dico%20digital%20de%20Valladolid.&uud=1&ct=1&pre=https%3A%2F%2Fwww.google.com%2F&tt=0&captcha_provider=recaptcha2&pro=0

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!")
}

How to download files in wkwebview

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.
}
}

Trigger a Button inside WebView

I have a HTML Page with different Buttons inside there which I opening with a WebView.
Now I will click on a button inside the WebView and will have an app action.
Is there a function in WebKit that gives a responds to such taps?
Using this delegate method you will track button action in the app.
let htmlString = "<a class=\"buton\" href=\"http://someaction\">Click</a>"
webView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL)
webView.navigationDelegate = self
WKNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
if let url = navigationAction.request.url {
if url.absoluteString == "http://someaction/" {
print("Action trigure")
} else {
print("No action trigure")
}
}
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}

How to know if a WKWebView is redirecting due to a user interaction?

I'm working with WKWebView and I need to execute some code when the user does something that cause the WKWebView to go to another url (e.g. click on a button in the WKWebView which leads to another url). I was trying to do it with comparing the working url with the original one, but some redirection are not caused my the user but the website itself. Is there anyway to differentiate? Here is what I have now:
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print(webView.url?.absoluteString)
if webView.url?.absoluteString != self.workingReult {
//do something
}
}
Thanks
sure. Here is my solution for Swift 4.
Once you set the navigationDelegate for the WKWebView, you can implement the following function which is called anytime the web view is navigating:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
//Tap on href link case
decisionHandler(WKNavigationActionPolicy.cancel)
} else {
//Any other case
decisionHandler(WKNavigationActionPolicy.allow)
}
}
As you can see you can get the trigger of the navigation action using navigationAction.navigationType. In my case I'm preventing the user from navigating the links but you can customize it as you like.
Remember that the decisionHandler must be included for any case.
You can check the rest of WKNavigationType constant cases here
You can use either of this,
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
let currentURL : NSString = (webView.URL!.host)!
//print("The current String is\(currentURL)")
if currentURL.containsString("test.com")
{
//handle your actions here
}
print("finish to load")
}
Or you can use this compare your url
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
let app = UIApplication.sharedApplication()
let url = navigationAction.request.URL
//handle your events from here
}
To see redirect URL, check URL from navigationAction instead of webview.url like this way.
override internal func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
super.webView(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
let request = navigationAction.request
if let url = request.url {
//here you can compare your url
}
}