Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WKScriptMessageHandler won't Listen to 'onclick' or 'click' event on a button element on a webpage. The web page is developed using Reactjs

I am using a WKWebView inside a UIViewController's view to display a webpage hosted on a server using a url endpoint. The webpage uses Reactjs. That is all the information I have about the webpage. The code creates a webview and inserts the webview as subview of the controllers view.

let requestObj = URL(string:urlString)!
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
webViewWK = WKWebView(frame: .zero, configuration: configuration)
webViewWK.navigationDelegate = self
_ = webViewWK.load(requestObj)
webViewwrapper = WKWebViewWrapper(forWebView: webViewWK)

The webpage loads fine and also the controller acts as the delegate of the webview and receives the messages for the same. Now I also implement a WKWebViewWrapper class which conforms to WKScriptMessageHandler. This class can then receive messages from webkit object which is created by the WKWebView behing the scenes. The implementation for the same is as below

class WKWebViewWrapper : NSObject, WKScriptMessageHandler{

var wkWebView : WKWebView
let eventNames = ["buttonClick"]
var eventFunctions: Dictionary<String, (String) -> Void> = [:]
let controller: WKUserContentController

init(forWebView webView : WKWebView){
    wkWebView = webView
    controller = WKUserContentController()
    super.init()
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if let contentBody = message.body as? String {
        if let eventFunction = eventFunctions[message.name]{
            print("Detected javascript event")
        }
    }
}

func setUpPlayerAndEventDelegation(){
    wkWebView.configuration.userContentController = controller
    for eventname in eventNames {
        controller.add(self, name: eventname)
        eventFunctions[eventname] = { _ in }

        wkWebView.evaluateJavaScript("var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); for (var i = 0 ; i < elements.length; i++) { elements[i].addEventListener('onClick', function(){ window.webkit.messageHandlers.\(eventname).postMessage(JSON.stringify(isSuccess)) }); }") { any, error in
            if let error = error {
                print("EvaluateJavaScript Error:",error)
            }

            if let any = any {
                print("EvaluateJavaScript anything:", any)
            }

        }
    }
}

}

The setUpPlayerAndEventDelegation() method is the most important part. Here for the controller object which is of type WKUserContentcontroller adds message handlers using its add(: , name:) method. According to documentation this method adds a messageHandler of the name parameter to the webkit object. Whenver the messsage handler is triggered, the WKScriptMessageHandler's userContentController( userContentController: WKUserContentController, didReceive message: WKScriptMessage) method is called with useful parameters. Then I inject javascript into the webpage using evaluateJavaScript method of webview which is as below

var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); 
for (var i = 0 ; i < elements.length; i++) {
    elements[i].addEventListener('onClick', function(){ window.webkit.messageHandlers.\(eventname).postMessage(JSON.stringify(true)) }); 
}

It fetches elements with the given class. Then I iterate over the array to add event listener for HTML event 'onClick' for each element. For events listener I add an anonymous function to trigger the previously registered message handler on the webkit. This script is executed properly as I don't get error in the completion block of the evaluateJavaScript method. So I can be sure now that when a button onClick HTML event occurs the annonymous function will execute, which in turn will postMessage for the messageHandler on the webkit object.

Now I call the WKWebViewWrapper's setUpPlayerAndEventDelegation() method from WKWebViewDelegate method webView(_ webView: WKWebView, didFinish navigation: WKNavigation!), where I can be sure that all the HTML elements are loaded by comapring WKNavigation objects.

The flow executes and after the Page loads and I click any buttons the events are not observed by my script message handler i.e the WKWebViewWrapper class. The method userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) is not fired at all.

Is there something that I am missing here?. I am not good at Javascript. Please do let me know if Reactjs needs some different script to and event listener to button elements. I have reffered this tutorial.

PS: If we add the similar script to output console messages on a webbrowser which has loaded the page, it works fine.

like image 289
Rohan Bhale Avatar asked Aug 24 '18 06:08

Rohan Bhale


2 Answers

Notice an important behavior (but less known) about WKWebViewConfiguration in Apple Docs,

WKWebViewConfiguration is only used when a web view is first initialized. You cannot use this class to change the web view's configuration after it has been created.

So, this is typically you should setup your WKUserContentController fully prior to web view creation.

// First, create custom configuration with user script
let userController = WKUserContentController()
let scalingScriptString = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
let scalingScript = WKUserScript(source: scalingScriptString, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userController.addUserScript(scalingScript)

let configurations = WKWebViewConfiguration()
configurations.userContentController = userController  // MUST set controller in configurations before creating webview
// Now, use that configuration to create the webview
webView = WKWebView(frame: .zero, configuration: configurations)
like image 67
Ashok Avatar answered Nov 15 '22 17:11

Ashok


So I actually ended up getting it to work. There were a number of issues but they were all due to errors in my JavaScript. The JavaScript simply failed to execute rather than producing any errors however which made it seem like it was an iOS problem. It took a really long time and lots of debugging via Safari.

Essentially what I discovered is probably a mistake that is rampant because of how little documentation/articles there are online for sending messages via WKMessagingScript. All of the samples show something like this:

window.webkit.messageHandlers.test.postMessage(“message to post”)

Some of them go on to say you can send anything even a json dictionary. What they fail to say is that 1) you can NOT pass an object to this function, and 2) passing a literal dictionary is illegal in JavaScript. They also don’t give any more applicable examples. You may want just a string message to let you know something has happened but if you need to pass data you’ll be getting it from the elements and are likely to make mistake number 1 in your implementation (what both you and I did).

1) is a biggie. In your example you are calling a function. In JavaScript functions are first class citizens so you are passing an object. You also can not, for example, pass element.id which is what I was doing because you have to pass the element. What you need to do is pass the value only which is a foundation type.

*** Note you can pass an object within JavaScript such as console.log(element); which is what makes debugging this issue so hard. If you had commented out the WebKit call but passed your function to a console log it would have worked, implying the problem was with iOS, rather than highlighting the problem was actually with passing an object.

2) will usually work in console logging because the browsers we use will recognize it. Enough devs do it that even though it’s not right the browsers will interpret it. iOS may also one day too but it’s better practice to not do it.


This would have worked (assuming no other issues in your code):

var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); 
for (var i = 0 ; i < elements.length; i++) {
    var message = String(JSON.stringify(true));
    elements[i].addEventListener('click', function(){ 
        window.webkit.messageHandlers.testEvent.postMessage(message) 
    }); 
}

Now I'm not totally sure if sending a string alone will still work or if it needs to be a dictionary as I was sending dictionaries but if you need to send a dictionary you would do it like this:

var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); 
for (var i = 0 ; i < elements.length; i++) {
    var eventName = String(eventName); // if this variable is a string then you probably don't need this step
    var stringified = String(JSON.stringify(true));
    var message = {};
    message[String(eventName)] = stringified;
    elements[i].addEventListener('click', function(){ 
        window.webkit.messageHandlers.testEvent.postMessage(message) 
    }); 
}

Rather than window.webkit.messageHandlers.testEvent.postMessage({"foo": "bar"}) for example. Note that I didn't test that this did not work, I just read this online and asked a JS dev I know and they confirmed it so who knows it may work. I think it's safer to break it up though just in case. There is some shorthand that has been added to JS using square brackets that would allow you to pass a literal however it is only recently added so I don't imagine all versions of iOS support it and I would not recommend using it.


I do see two additional problems with your code though. First is that you are using 'onClick' when you should be using 'click'.

So onclick creates an attribute within the binded HTML tag, using a string which is linked to a function. Whereas .click binds the function itself to the property element. https://teamtreehouse.com/community/whats-the-difference-between-click-and-onclick

If you were a web dev you would handle both but for iOS you should only use click.

The other thing I noticed in your code is you are passing the eventName into the webkit function window.webkit.messageHandlers.\(eventName).postMessage...... I'm not totally sure if that will work or not. I suspect it will not because that is not a string, that is a function call. Though I don't know anything about JavaScript at all (this was literally the first JS I've ever written) so I may be wrong about that. In objc or swift though you could not do that when making a function call. Even if it would work I think it adds too much complexity and is not scalable if the iOS WKMessagingScript were updated to no longer allow it. I would suggest using the correct name. If you want to encapsulate your code then switch on eventName.

like image 27
Abbey Jackson Avatar answered Nov 15 '22 16:11

Abbey Jackson