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
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've switched one of my apps to use WKWebview and it's the first time for me, I had a problem which is as you know I can't control it from the storyboard, I have a scrollview inside a sidebar that comes out when you click the button on the upper left, but it seems like it appears in the back of the view, how can I fix this please?
let configuration = WKWebViewConfiguration()
configuration.preferences = preference
webView = WKWebView(frame: view.bounds, configuration: configuration)
view.addSubview(webView)
please note that I already clicked the button in that screeshot but the scroll didn't show]1
Use this,
first import
import WebKit
set delegate of WKWebview
WKScriptMessageHandler , WKNavigationDelegate
declare webview
var webView: WKWebView!
var webConfiguration:WKWebViewConfiguration! = nil
override func loadView() {
webView = WKWebView(frame: .zero, configuration: self.webConfig())
webView.navigationDelegate = self
view = webView
}
viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
/*if let url = URL(string: "http://192.168.1.122/arvee/piyush/mobileViewPages/owner/manager.html") {
let request = URLRequest(url: url)
webView.load(request)
}*/
let url = Bundle.main.url(forResource: "AddAppointment/bookApp", withExtension:"html")
let request = URLRequest(url: url!)
webView.load(request)
}
WKWebview Delegate
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print(error)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let data = JSON(message.body)
}
func webConfig() -> WKWebViewConfiguration {
//if webConfiguration != false {
webConfiguration = WKWebViewConfiguration()
let userController = WKUserContentController()
userController.add(self, name: "buttonClicked")
userController.add(self, name: "pageLoaded")
webConfiguration.userContentController = userController
//}
return webConfiguration
}
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.