I am creating a View which displays a local webpage which also has styles and JavaScript (Essentially a hybrid native application) and I am using a WKWebView.
I know how to send data from the JavaScript to the native code but it is not clear how to do it the reverse way. I am wanting to send a JSON Object that is fetched using native iOS code to my MKWebView.
I am getting the following error
Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo=0x7bfb3c30 {NSLocalizedDescription=A JavaScript exception occurred}
nil
Below is my code.
//
// WebViewController.swift
//
// Created by Adam Bulmer on 09/03/2015.
// Copyright (c) 2015 Adam Bulmer. All rights reserved.
//
import Foundation
import WebKit
class WebViewController: BaseViewController, WKScriptMessageHandler, WKNavigationDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func loadView() {
super.loadView()
var config = WKWebViewConfiguration();
var contentController = WKUserContentController();
var userScript = WKUserScript(
source: "bootstrap()",
injectionTime: WKUserScriptInjectionTime.AtDocumentEnd,
forMainFrameOnly: true
)
contentController.addUserScript(userScript)
config.userContentController = contentController;
self.webView = WKWebView(frame:self.view.frame, configuration: config)
self.webView.navigationDelegate = self;
self.view = self.webView!
}
override func viewDidLoad() {
super.viewDidLoad()
var path = NSBundle.mainBundle().pathForResource(webViewName(),
ofType: "html");
var url = NSURL(fileURLWithPath: path!);
var request = NSURLRequest(URL: url!);
self.webView!.loadRequest(request);
}
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
var param = "Hello World";
var exec_template = "test(\(param)');";
self.webView!.evaluateJavaScript(exec_template, completionHandler: { (test, error) -> Void in
println(error);
println(test);
})
}
func webViewName() -> String {
return "";
}
// MARK: WKScriptMessageHandler callback
// Delegate callback for when scripts sends message
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
}
}
This code:
var param = "Hello World";
var exec_template = "test(\(param)');";
Will result in ..
test(Hello World');
.. being executed, which is not valid JS. You missed the opening ' in your code.
Related
class FeedBackFormViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
#IBOutlet weak var webViewShowing: UIView!
var formWebView: WKWebView!
private let fileString = UserSingleton.shared.feedbackFormLink
override func viewDidLoad() {
super.viewDidLoad()
setupJSFile()
formWebView.navigationDelegate = self
previewFiles()
}
// show files in web view
private func previewFiles() {
if let fileString = fileString, fileString != "" {
let url = URL(string: fileString)
let myRequest = URLRequest(url: url!)
UserSingleton.shared.showHUD()
formWebView.load(myRequest)
}else {
Alerts.shared.show(alert: .error, message: "no file found", type: .error)
}
}
private func setupJSFile() {
let config = WKWebViewConfiguration()
let js = "document.addEventListener('submit', function(){ window.webkit.messageHandlers.clickListener.postMessage('My hovercraft is full of eels!'); })"
let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
config.userContentController.addUserScript(script)
config.userContentController.add(self, name: "submit")
formWebView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webViewShowing.addSubview(formWebView)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.name)
}
#IBAction func dismissFeedbackFormVC(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
extension FeedBackFormViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
webView.evaluateJavaScript("document.readyState") { (result, error) in
if let result = result {
print(result)
}
}
}
}
only call wk navigation when the web view is loaded and I click anywhere no trigger call in userContentController any event
You'd need to add this script for execution, just append it at the end of the func setupJSFile:
formWebView.configuration.userContentController.addUserScript(script)
also, you'd need to change your JS script to
document.addEventListener('click', function(){ window.webkit.messageHandlers['iosListener'].postMessage('click clack!'); })
I have a UIWebKit loaded in an url and I want to pick the id of a html element when clicked. I can get the element when I know the id but how to get an unknown element Id when it is clicked.
thank you for helping me !
You can do it following way.
Inject some javascript into the WebView using WKUserScript.
Injected javascript will listen to document body for any click event.
Upon an click event received, find the DOM element using elementFromPoint.
Setup that way, javascript will communicate with native code.
Upon receiving clicks and finding the element, contact native code for clicked DOM.
I've tested this way of working & it works for me.
import UIKit
import WebKit
class ViewController: UIViewController {
#IBOutlet weak var webView: WKWebView!
private var url = URL(string: "https://www.google.com")!
override func viewDidLoad() {
super.viewDidLoad()
initializeWebView()
loadData()
}
private func initializeWebView() {
let javascript = """
window.onload = function() {
document.addEventListener("click", function(evt) {
var tagClicked = document.elementFromPoint(evt.clientX, evt.clientY);
window.webkit.messageHandlers.jsMessenger.postMessage(tagClicked.outerHTML.toString());
});
}
"""
let userScript = WKUserScript.init(source: javascript,
injectionTime: .atDocumentStart, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)
webView.configuration.userContentController.add(self, name: "jsMessenger")
}
private func loadData() {
let request = URLRequest(url: url)
webView?.load(URLRequest.init(url: url))
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}
}
It gives you the whole element as string
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet weak var webContentView: UIView!
var web: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let JSsource = """
window.onload = function() {
document.addEventListener("click", function(evt) {
var tagClicked = document.elementFromPoint(evt.clientX, evt.clientY);
window.webkit.messageHandlers.jsMessenger.postMessage(tagClicked.outerHTML.toString());
});
}
"""
let script = WKUserScript(source: JSsource, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
config.userContentController.addUserScript(script)
config.userContentController.add(self as! WKScriptMessageHandler, name: "jsMessenger")
self.web = WKWebView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height-60), configuration : config)
self.web.navigationDelegate = self
self.web.uiDelegate = self
self.webContentView.addSubview(self.web!)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}
I am trying to parse the html returned from a WKWebView load() with evaluateJavaScript but it never prints anything. Am I doing this right? Any other ways? didFinish does print.
import UIKit
import WebKit
class MyWebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: self.view.frame)
webView.navigationDelegate = self
let url = NSURL (string: "https://google.com");
let request = NSURLRequest(url: url! as URL)
webView.load(request as URLRequest)
self.view.addSubview(webView)
self.view.sendSubview(toBack: webView)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: { (html: AnyObject?, error: NSError?) in
print(html!)
} as? (Any?, Error?) -> Void)
print("didFinish")
}
}
Using evaluateJavaScript with a WKWebView is a bit tricky.
Since I think this answer would be useful to many people, rather than address your specific question with a short code snippet and a comment that you need to implement WKScriptMessageHandler, I'm going to post a full, complete example that you can use to see how everything works together.
To use this, create a "Single View Application" iOS project in Xcode and paste this over the default ViewController.swift file.
//
// WebViewController.swift
// WKWebViewExample
//
// Created by par on 4/2/17.
// Copyright © 2017 par. All rights reserved. MIT License.
//
import UIKit
import WebKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let webViewController = WebViewController()
// install the WebViewController as a child view controller
addChildViewController(webViewController)
let webViewControllerView = webViewController.view!
view.addSubview(webViewControllerView)
webViewControllerView.translatesAutoresizingMaskIntoConstraints = false
webViewControllerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webViewControllerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
webViewControllerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
webViewControllerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
webViewController.didMove(toParentViewController: self)
}
}
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
private var webView: WKWebView!
private var webViewContentIsLoaded = false
init() {
super.init(nibName: nil, bundle: nil)
self.webView = {
let contentController = WKUserContentController()
contentController.add(self, name: "WebViewControllerMessageHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.scrollView.bounces = false
webView.navigationDelegate = self
return webView
}()
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
webView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !webViewContentIsLoaded {
let url = URL(string: "https://stackoverflow.com")!
let request = URLRequest(url: url)
webView.load(request)
webViewContentIsLoaded = true
}
}
private func evaluateJavascript(_ javascript: String, sourceURL: String? = nil, completion: ((_ error: String?) -> Void)? = nil) {
var javascript = javascript
// Adding a sourceURL comment makes the javascript source visible when debugging the simulator via Safari in Mac OS
if let sourceURL = sourceURL {
javascript = "//# sourceURL=\(sourceURL).js\n" + javascript
}
webView.evaluateJavaScript(javascript) { _, error in
completion?(error?.localizedDescription)
}
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// This must be valid javascript! Critically don't forget to terminate statements with either a newline or semicolon!
let javascript =
"var outerHTML = document.documentElement.outerHTML.toString()\n" +
"var message = {\"type\": \"outerHTML\", \"outerHTML\": outerHTML }\n" +
"window.webkit.messageHandlers.WebViewControllerMessageHandler.postMessage(message)\n"
evaluateJavascript(javascript, sourceURL: "getOuterHMTL")
}
// MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let type = body["type"] as? String else {
print("could not convert body[\"type\"] to string: \(body)")
return
}
switch type {
case "outerHTML":
guard let outerHTML = body["outerHTML"] as? String else {
print("could not convert body[\"outerHTML\"] to string: \(body)")
return
}
print("outerHTML is \(outerHTML)")
default:
print("unknown message type \(type)")
return
}
}
}
I know it has been a while since this was answered and I found that this didn't really work for me. I used the below instead.
I could not get the evaluateJavaScript to work until i added the setTimeout() function. For what it is worth here is my code below.
webView.evaluateJavaScript("setTimeout(function(){
// Do something here
},10);") { (result1, error) in
if error == nil {
print(result1 ?? "")
}
}
How can I to create the JavascriptInterface channel from my web site to my UIWebView?
Example in Android:
webView.addJavascriptInterface(new WebAppInterface(this), "Android");
And from this JavascriptInterface I would like to draw the methods, as for example:
func webViewDidStartLoad(webView: UIWebView)
or
myActivityIndicator.startAnimating()
How can I do?
For WKWebView: source here
JavaScript
function callNativeApp () {
try {
webkit.messageHandlers.callbackHandler.postMessage("Hello from JavaScript");
} catch(err) {
console.log('The native context does not exist yet');
}
}
Swift
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler {
#IBOutlet var containerView: UIView? = nil
var webView: WKWebView?
override func loadView() {
super.loadView()
let contentController = WKUserContentController()
contentController.addScriptMessageHandler(self, name: "callbackHandler")
let config = WKWebViewConfiguration()
config.userContentController = contentController
self.webView = WKWebView( frame: self.containerView!.bounds, configuration: config)
self.view = self.webView
}
override func viewDidLoad() {
super.viewDidLoad()
//I use the file html in local
let path = NSBundle.mainBundle().pathForResource("index", ofType: "html")
let url = NSURL(fileURLWithPath: path!)
let req = NSURLRequest(URL: url)
self.webView!.loadRequest(req)
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {// edit: changed fun to func
if (message.name == "callbackHandler"){
print("\(message.body)")
}
}
}
For UIWebView: source here
JavaScript in HTML
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script>
(function(){
$(window).load(function(){
$('.clickMe').on('click', function(){
window.location = "foobar://fizz?Hello_from_javaScript";
});
});
})(jQuery);
</script>
Swift
import UIKit
class ViewController: UIViewController, UIWebViewDelegate {
#IBOutlet weak var Web: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
let path = NSBundle.mainBundle().pathForResource("index", ofType: "html")
let url = NSURL(fileURLWithPath: path!)
let req = NSURLRequest(URL: url)
Web.delegate = self
Web.loadRequest(req)
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if request.URL?.query != nil {
print("\(request.URL!.query!)")
}
return true
}
}
Here's a quick example:
Register a URL Scheme such as foobar in Xcode
Handle this URL Scheme in your web view
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if request.URL?.query?.containsString("show_activity_indicator=true") {
myActivityIndicator.startAnimating()
}
}
Finally, call this from your JavaScript
// Show your activity indicator from JavaScript.
window.location = "foobar://fizz?show_activity_indicator=true"
Note: See my question here for more information on web view communication in iOS.
I use WKWebView and I want to be notified when website is fully loaded. The webView:didFinishNavigation method of WKNavigationDelegate is fired when document.readyState is either interactive or complete and I want to be sure that site was completely loaded. I came up with the solution which uses JavaScript injection. Here is my MWE:
import UIKit
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
#IBOutlet weak var loadLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
let scriptPath = NSBundle.mainBundle().pathForResource("script", ofType: "js")!
let scriptString = try! String(contentsOfFile: scriptPath)
let script = WKUserScript(source: scriptString, injectionTime: .AtDocumentStart, forMainFrameOnly: true)
contentController.addUserScript(script)
contentController.addScriptMessageHandler(self, name: "readyHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = self
loadLabel.text = nil
}
#IBAction func loadWebsite() {
webView.loadRequest(NSURLRequest(URL: NSURL(string: "http://stackoverflow.com")!))
loadLabel.text = "Loading..."
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print("message received")
loadLabel.text = "Complete"
}
}
And this is the content of script.js file:
document.onreadystatechange = function () {
if(document.readyState === "complete"){
webkit.messageHandlers.readyHandler.postMessage("");
}
}
userContentController:didReceiveScriptMessage method is always called on iOS Simulator, but on the actual device (iPhone 6 in my case) it isn't called most of the times. Any idea what can be wrong about it or what's the other way of checking if website is completely loaded?
For some reason you need to add the webView to a visible view for this to work on the device. If you don't want the webView to be visible, add it and then set the hidden property to true.
For the code example above:
func viewDidLoad(){
...
webView.hidden = true
view.addSubview(webView)
}