I'm trying to write a command line tool that takes a screenshot of a given webpage using WKWebView. The problem is that WKNavigationDelegate methods aren't being called. This is what I have:
import WebKit
class Main: NSObject {
let webView: WKWebView = WKWebView()
func load(request: URLRequest) {
webView.navigationDelegate = self
webView.load(request)
}
}
extension Main: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("Did start")
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("Did commit")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Did finish")
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Did fail")
}
}
let main: Main = Main()
let input: String = CommandLine.arguments[1]
if let url: URL = URL(string: input) {
let request: URLRequest = URLRequest(url: url)
main.load(request: request)
} else {
print("Invalid URL")
}
Almost all examples I've found involve using WKWebView in a view controller. My guess is that in the command line, the app exits before the webpage finishes loading, but I'm not sure how to prevent that from happening.
I did find this example of a command line tool using WKWebView. The author uses RunLoop.main.run(), which to my understanding effectively simulates the event loop of a UI app? That allows the webpage to load, but I'm looking for a different solution because I want the app to behave like a normal command line tool and exit on its own after running. For example, is there some way to use async/await with WKWebView.load() much like with URLSession?
I ended up solving this problem using continuation. In short, I wrap webView.load() in a continuation and then call continuation.resume() in one of the WKNavigationDelegate methods. That allows me to treat webView.load() as an async task; the continuation determines its runtime. I took this solution entirely from the example in this blog post.
Here's a barebones implementation of this solution:
import WebKit
#MainActor
class WebContainer: NSObject {
lazy var webView: WKWebView = {
let webView: WKWebView = WKWebView()
webView.navigationDelegate = self
return webView
}()
var continuation: UnsafeContinuation<Void, Error>?
func load(request: URLRequest) async throws -> Int {
try await withUnsafeThrowingContinuation { continuation in
self.continuation = continuation
webView.load(request)
}
}
}
extension WebContainer: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
continuation?.resume(returning: ())
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
continuation?.resume(throwing: error)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
continuation?.resume(throwing: error)
}
}
To load a website using the above implementation, simply run:
let webContainer: WebContainer = WebContainer()
let request: URLRequest = // insert URLRequest here
try await webContainer.load(request: request)
Practically speaking this implementation doesn't handle redirects reliably as some redirects are initiated after didFinish is called. I have a partial solution to that which however runs into other problems. Since all this is out of scope for this question, if anyone's interested please refer to this other question.
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
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.
}
}
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)
}
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
}
}