I am trying to retrieve html using webView. I don't want to write my webView code in the view file therefore trying to implement it in a different class. Below is my code.
class BlobHelper: NSObject,WKNavigationDelegate{
let webView = WKWebView()
func getLyrics(){
let url = URL(string: "https://Blob")!
let request = URLRequest(url: url)
webView.load(request)
webView.evaluateJavaScript("document.getElementsByTagName('html')[0].innerHTML;", completionHandler: {(value,error)in
print(value)
print(error)
})
}
func webView(_ webView: WKWebView,didFinish navigation: WKNavigation!) { print("loaded") }
When I execute the code I get Optional(Error Domain=WKErrorDomain Code=3 "The WKWebView was invalidated" UserInfo={NSLocalizedDescription=The WKWebView was invalidated}).
What I tried
I was able to retrieve html using static but couldn't get the didFinish to call after loading.
Any help is appreciated.
class ViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let configuration = WKWebViewConfiguration()
self.webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = self
let url = URL(string: "https://stackoverflow.com")!
let request = URLRequest(url: url)
webView.load(request)
self.view = webView
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.getElementsByTagName('html')[0].innerHTML;", completionHandler: { (value,error)in
print(value)
})
}}
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'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
}
}
what I'm trying to do is perform an action only after the WKWebView I'm using has finished loading and scraping the page.
Here's the code for the webView:
let webView = WKWebView()
let url = URL(string: "https://web.spaggiari.eu/home/app/default/menu_webinfoschool_genitori.php?custcode=")!
let request = URLRequest(url: url)
webView.load(request)
What I want to do is show the button to "Connect" only after the process has fished (that specific process and not when the webView finishes loading every time). For now I'm using DispatchQueue to wait for seconds hoping the page loads in time.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) {
UIView.animate(withDuration: 0.8, animations: {
self.registerButton.alpha = 1
self.registerButton.isEnabled = true
})
UIView.animate(withDuration: 0.8, delay: 0.4, animations: {
self.blurButton.alpha = 1
})
}
I'm using Xcode with Swift3
this code indicates that page fully loaded, may this help you
func webpageFullyLoaded(_ str: String) {
if (str == "complete") || (str == "interactive") {
print("page completely loaded")
}
}
func webViewDidFinishLoad(_ webView: UIWebView) {
webpageFullyLoaded(webView.stringByEvaluatingJavaScript(from: "document.readyState"))
}
WKWebView has this delegate
/*! #abstract The web view's navigation delegate. */
weak open var navigationDelegate: WKNavigationDelegate?
WKNavigationDelegate :
A class conforming to the WKNavigationDelegate protocol can provide
methods for tracking progress for main frame navigations and for deciding
policy for main frame and subframe navigations
This delegate has a method, that might help you
/*! #abstract Invoked when a main frame navigation completes.
#param webView The web view invoking the delegate method.
#param navigation The navigation.
*/
#available(iOS 8.0, *)
optional public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
So you can try this
func loadPage() {
let webView = WKWebView()
webView.navigationDelegate = self
let url = URL(string: "https://web.spaggiari.eu/home/app/default/menu_webinfoschool_genitori.php?custcode=")!
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Do your work here
}
Also make sure your self here conforms to WKNavigationDelegate.
Update
To handle loading of different web pages in the same WKWebView you can,
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Swift.Void) {
print("Page Being loaded is \(navigationAction.request), you can do your work here")
decisionHandler(.allow)
}
You can read more about what you can do in the navigation delegate here
This question already has answers here:
Transport security has blocked a cleartext HTTP
(29 answers)
Closed 5 years ago.
before this get's shot down for being a duplicate, it isn't. Pretty much every question on WKWebView here is about WKWebView in iOS Apps, not macOS Apps, with the difference being pretty much just the UIViewController interface being implemented instead of the NSViewController interface in macOS.
The example code in Apple's Documentation as well as the Controller code, that can be found online doesn't work. Altough it does compile without a problem the webview stays inactive.
Is there something that I just didn't see or is this a bug in WKWebView ?
I even copied some code from tutorials showing how to do this for iOS and just changed UIViewController to NSViewController (since that was the ONLY difference), yet it didn't work.
The following code in ViewController.swift does not work.
It also doesn't work if it's
class ViewController: NSViewController, WKUIDelegate
import Cocoa;
import WebKit;
class ViewController: NSViewController {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let url=URL(string: "http://safetec-cam.biz/images/webcam_extern/bad-nauheim_bahnhof_west.jpg");
webView.load(URLRequest(url: url!));
}
}
it also doesn't work if it's done like this with the UIViewController exchanged for NSViewController
image from https://developer.apple.com/documentation/webkit/wkwebview
I recommend you to start from scratch:
Set Your URL to be loaded:
let myURLString = "https:yourWebLink"
let url = URL(string: myURLString)
let request = URLRequest(url: url!)
Init and load request in webview:
let webView = WKWebView(frame: self.view.frame)
webView.navigationDelegate = self
webView.load(request)
Implement WKNavigationDelegate to trace your page Load/Error:
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("Started to load")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finished loading")
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
print(error.localizedDescription)
}
}
For further reference check: https://iosdevcenters.blogspot.com/2016/05/creating-simple-browser-with-wkwebview.html