Handling JavaScript events in WKWebView
you can use a WKUserScript and add it to the userContentController of the WKWebView's configuration.
let config = WKWebViewConfiguration()
let source = "document.addEventListener('click', function(){ window.webkit.messageHandlers.iosListener.postMessage('click clack!'); })"
let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
config.userContentController.addUserScript(script)
config.userContentController.add(self, name: "iosListener")
webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
this will make the script and inject it into the page when the document is finished loading. Now, you need to implement the WKScriptMessageHandler protocol to receive the message:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("message: \(message.body)")
// and whatever other actions you want to take
}
Catch Javascript Event in iOS WKWebview with Swift
Based on the answer from @Alex Pelletier, which really helped me, here is the solution the my question.
In my "loadView()" function, here is what I have :
let contentController = WKUserContentController();
contentController.addScriptMessageHandler(
self,
name: "callbackHandler"
)
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: CGRectZero, configuration: config)
webView.navigationDelegate = self
view = webView
My function to handle the Javascript event which is sent to Swift :
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
{
if(message.name == "callbackHandler") {
print("Launch my Native Camera")
}
}
... And finally, my Javascript (jQuery) code when a click happens on my camera button (in HTML) :
$(document).ready(function() {
function callNativeApp () {
try {
webkit.messageHandlers.callbackHandler.postMessage("camera");
} catch(err) {
console.log('The native context does not exist yet');
}
}
$(".menu-camera-icon").click(function() {
callNativeApp();
});
});
I hope it will help someone else :-) !
WKScriptMessageHandler won't Listen to 'onclick' or 'click' event on a button element on a webpage. The web page is developed using Reactjs
After 4 years I got a chance to work on the similar codebase and found out the exact reason of the failure. The exact reason are as below:
- Unnecessary and erroneous assignment of
userContentController
. This didn't replace the actualWKUserContentController
created at the time of instantiation ofWKWebView
. We added the script handler erroneously like socontroller.add(self, name: eventname)
. Instead we should have added the script handler directly to the webviews usercontentcontroller instance like so
//WRONG: To set userContentController and add script handler
wkWebView.configuration.userContentController = controller
controller.add(self, name: eventname)
//CORRECT: To add script handler
wkWebView.configuration.userContentController.add(self, name: eventname)
Thanks to @Ashok for pointing this out.
- The timing of evaluating the javascript to add Event Listener to each element was not perfect. By the time our javascript got evaluated the elements had not been created and thus no event listeners were added. There are two ways to perfect the time of evaluation of the javascript. We can introduce a delay which obviously is not a clean solution. The cleaner solution is to use
WKUserScript
with theinjectionTime
set to.atDocumentEnd
and add the script to theuserContentController
let eventName = "SOME_EVENT"
let js = "var elements = document.getElementsByClassName('btn button_btn button_primary button_md button_block'); for (var i = 0 ; i < elements.length; i++) { elements[i].addEventListener('click', function(){ window.webkit.messageHandlers.\(eventname).postMessage("MESSAGE") }); }"
let userScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
wkWebView.configuration.userContentController.addUserScript(userScript)
- I would like to thanks @Abbey Jackson for providing hints to the solution for my case. Her answer pointed me to the fact that
onClick
,onclick
should not be used for adding event listeners to the elements. Instead useclick
for the same.
// Relevant while coding for WKWebView on iOS
// WRONG: Using onClick or onclick does not register the event listener
elements[i].addEventListener("click", function(){ YOUR_HANDLER_CODE } }
// CORRECT: Use click instead of onClick or onclick for adding event listener
elements[i].addEventListener("click", function(){ YOUR_HANDLER_CODE } }
NOTE: I could pefectly pass a literal dictionary like {"foo": "bar"} in place of "YOUR_MESSAGE". It didnt cause me a problem. Although it could cause a problem if it contained items which couldn't be easily ported to Swift.
How to inject JavaScript callback to detect onclick event, using iOS WKWebView?
User Scripts are JS that you inject into your web page at either the start of the document load or after the document is done loading. User scripts are extremely powerful because they allow client-side customization of web page, allow injection of event listeners and can even be used to inject scripts that can in turn call back into the Native app. The following code snippet creates a user script that is injected at end of document load. The user script is added to the WKUserContentController instance that is a property on the WKWebViewConfiguration object.
// Create WKWebViewConfiguration instance
var webCfg:WKWebViewConfiguration = WKWebViewConfiguration()
// Setup WKUserContentController instance for injecting user script
var userController:WKUserContentController = WKUserContentController()
// Get script that's to be injected into the document
let js:String = buttonClickEventTriggeredScriptToAddToDocument()
// Specify when and where and what user script needs to be injected into the web document
var userScript:WKUserScript = WKUserScript(source: js,
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: false)
// Add the user script to the WKUserContentController instance
userController.addUserScript(userScript)
// Configure the WKWebViewConfiguration instance with the WKUserContentController
webCfg.userContentController = userController;
Your web page can post messages to your native app via the window.webkit.messageHandlers.<name>.postMessage (<message body>)
method.
Here, “name” is the name of the message being posted back. The JS can post back any JS object as message body and the JS object would be automatically mapped to corresponding Swift native object.
The following JS code snippet posts back a message when a button click event occurs on a button with Id “ClickMeButton”.
var button = document.getElementById("clickMeButton");
button.addEventListener("click", function() {
varmessageToPost = {'ButtonId':'clickMeButton'};
window.webkit.messageHandlers.buttonClicked.postMessage(messageToPost);
},false);
In order to receive messages posted by your web page, your native app needs to implement the WKScriptMessageHandler protocol.
The protocol defines a single required method. The WKScriptMessage instance returned in the callback can be queried for details on the message being posted back.
func userContentController(userContentController: WKUserContentController,
didReceiveScriptMessage message: WKScriptMessage) {
if let messageBody:NSDictionary= message.body as? NSDictionary{
// Do stuff with messageBody
}
}
Finally, the native class that implements WKScriptMessageHandler protocol needs to register itself as a message handler with the WKWebView as follows:
// Add a script message handler for receiving "buttonClicked" event notifications posted
// from the JS document
userController.addScriptMessageHandler(self, name: "buttonClicked")
Adding a click event listener on WKWebView not working
For some reason, Everything started working after adding the code after evaluating that javascript. I am still unable to know why.
Here is the code for the full extension so far:
extension WKWebView {
/// Type of HTML element to get from DOM
enum ElementType {
/// ID Element
case id
/// Class element
case `class`
}
/// List of errors for WKWebView injection
enum InjectionError: Error {
/// The Listener is already added
case listenerAlreadyAdded
}
/// Cahnges the CSS Visibiltiy
/// - Parameters:
/// - elementID: The name of the element
/// - isVisible: Wether or not is visible
/// - elementType: The type of element to get
/// - completion: Callback triggered went script has been appended to WKWebView
func changeCSSVisibility(elementID: String, isVisible: Bool, elementType: ElementType, completion: ((Error?)->Void)?) {
let script: String
switch elementType {
case .id:
script = "document.getElementById('\(elementID)').style.visibility = \(isVisible ? "'visible'":"'hidden'");"
case .class:
script =
"""
[].forEach.call(document.querySelectorAll('.\(elementID)'), function (el) {
el.style.visibility = \(isVisible ? "'visible'":"'hidden'");
});
"""
}
evaluateJavaScript(script, completionHandler: { (result, error) in
completion?(error)
})
}
/// Adds a event listener that will be call on WKScriptMessageHandler - didReceiveMessage
/// - Parameters:
/// - elementID: The name of the element
/// - callbackID: The ID for the callback
/// - elementType: The type of element to get
/// - completion: Callback triggered went script has been appended to WKWebView
func addEventListener(elementID: String, callbackID: String, elementType: ElementType, handler: WKScriptMessageHandler, completion: ((Error?)->Void)?) {
let element: String
switch elementType {
case .id:
element = "document.getElementById('\(elementID)')"
case .class:
element = "document.getElementsByClassName('\(elementID)')[0]"
}
let scriptString = """
function callback () {
console.log('\(callbackID) clicked!')
window.webkit.messageHandlers.\(callbackID).postMessage({
message: 'WKWebView-onClickListener-\(callbackID)'
});
}
\(element).addEventListener('click', callback);
"""
if configuration.userContentController.userScripts.first(where: { $0.source == scriptString }) == nil {
evaluateJavaScript(scriptString) { [weak self] (result, error) -> Void in
guard let self = self else { return }
if let error = error {
completion?(error)
} else {
self.configuration.userContentController.removeScriptMessageHandler(forName: callbackID)
self.configuration.userContentController.add(handler, name: callbackID)
self.configuration.userContentController.addUserScript(WKUserScript(source: scriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
}
}
} else {
completion?(InjectionError.listenerAlreadyAdded)
}
}
}
Issue with observing WKWebView URL changes via JavaScript events
Before you go any further, I would strongly recommend that you use the modern Swift method of observing:
var webViewURLObserver: NSKeyValueObservation?
override func viewDidLoad() {
// ...
webViewURLObserver = webview.observe(\.url, options: .new) { webview, change in
print("URL: \(String(describing: change.newValue))")
// The webview parameter is the webview whose url changed
// The change parameter is a NSKeyvalueObservedChange
// n.b.: you don't have to deregister the observation;
// this is handled automatically when webViewURLObserver is dealloced.
}
}
Doing this makes your code much cleaner and easier to reason about while you're debugging the issue.
Related Topics
Trigger Same CSS Animation on Every Click
Using Variables in Gulp for the Destination File Name
Html5 Video Custom Additional Seek Bar
Jquery Calendar Time Selection Suggestion
How to Position Popover Over a Highlighted Portion of Text
How to Grab the CSS Truncated Text via Jquery
Document.Createelement('Div') with a Class
How to Make a Pointy Arrow with a Div in CSS
Making an Element Visible by Hovering Another Element (Without :Hover-Property)
Update Progressbar in Each Loop
Jquery Word for Word Fade in Effect
Single Navigation Bar Across Website
Get Document.Stylesheets by Name Instead of Index