WKWebView not able to trigger javascript in the loaded Web Page.
Scenario:
If user click image in Website, it should get update.
Using javascript to update the image on the website, if the user clicks a image.
included .js file in project
configured WKWebview
Enabled JavaScript
Added Script in WKWebview
Function in JS file like :
function captureImage(bucket,fileName){
window.webkit.messageHandlers.captureImage.postMessage("showCamera")
}
Accessing this function in Swift like:
webViewPWA.configuration.userContentController.add(self, name: "captureImage")
///This function handles the event generated by javascript
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("Webview Message received: \(message.name) with body: \(message.body)")
if (message.name == "captureImage"){
print("\(message.body)")
let body = message.body
if let action:String = body as? String {
switch action {
case "showCamera":
print("camera image triggering, capture image for JS")
//take necessary action
break
default:
break
}
}
}
return
}
Thanks in advance!
Once the picture is captured on the device in UIImagePickerControllerDelegate method like:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
imagePicker.dismiss(animated: true, completion: nil)
imageView.image = info[.originalImage] as? UIImage
}
You can run any JS you want in a WKWebView by using evaluateJavaScript() and get a result in Swift.
webView.evaluateJavaScript("document.getElementById('someElement').innerText") { (response, error) in
guard error == nil else {
print("evaluateJavaScript error -- \(String(describing: error))")
return
}
print("evaluateJavaScript response -- \(String(describing: response))")
}
it would also be nice to have something similar to WebBridge.js that provides functions for communicating with the build ing WKWebView in iOS and he android webview
and inside of the WebBridge.js you can define:
/* install global handler for WebView to call methods */
if (!window.WebBridge) {
window.WebBridge = (function () {
var actions = []
return {
receive: function (actionName, json) {
if (typeof actions[actionName] !== 'undefined') {
actions[actionName](json)
}
},
registerActionHandler: function (actionName, method) {
actions[actionName] = method
}
}
})()
}
then from Swift file you can narrow down the structure of your JS calls:
self.webView.evaluateJavaScript("WebBridge.receive('yourCustomActionName', '{\"yourRey\": \"yourValue\"}')") { (response, error) in
guard error == nil else {
print("evaluateJavaScript error -- \(String(describing: error))")
return
}
print("evaluateJavaScript response -- \(String(describing: response))")
}
Related
I am working on a project where I want to integrate React-Native into a native Swift app. To make sure both sides are aware of state, I've made a 'message bus', A mechanism through which events can be passed from Javascript to native, and vice versa.
This works like a charm when sending an event from JS to iOS; it gets received, parsed and my Swift code knows exactly what to do. Sending an event from Swift to Javascript seems a lot harder - and poorly documented - as I find myself stuck on an error:
terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error when sending event: RCTCrossPlatformEventBus.Event with body: { "name" : "Authenticated", "data" : "{\"firstName\":\"1\",\"email\":\"34\",\"lastName\":\"2\",\"password\":\"3\"}" }. RCTCallableJSModules is not set. This is probably because you've explicitly synthesized the RCTCallableJSModules in RCTEventEmitter, even though it's inherited from RCTEventEmitter.'
This error seems to be common as there are a lot of stack overflow questions and GitHub issues to be found on it. However, nearly all of them date back from ±5 years ago, and simply don't help me with my issue any more. Below, I'm listing my implementation. If anybody could provide me with some guidance on how to solve this issue, it would be highly appreciated.
RCTCrossPlatformEventBus.m
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
#interface RCT_EXTERN_MODULE(RCTCrossPlatformEventBus, RCTEventEmitter)
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(processHybridEvent: (NSString *)name) // this receives JS events
#end
RCTCrossPlatformEventBus.swift
#objc(RCTCrossPlatformEventBus)
open class RCTCrossPlatformEventBus: RCTEventEmitter {
override init() {
super.init()
}
static let appShared = RCTCrossPlatformEventBus()
#objc
public override static func requiresMainQueueSetup() -> Bool {
return true
}
/// Processes a received event received from hybrid code
/// - Parameters:
/// - json: the json encoded string that was sent
#objc func processHybridEvent(_ json: String) {
print("Swift Native processing event: \(json)")
DispatchQueue.main.async {
var jsonObject: [String: Any]?
if let jsonData = json.data(using: .utf8), let dict = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String:Any] {
jsonObject = dict
}
NotificationCenter.default.post(name: .RCTCrossPlatformEventBusEvent, object: self, userInfo: jsonObject)
}
}
/// Posts an event to both the hybrid code
/// - Parameters:
/// - json: the json encoded string that will be sent
#objc func postEvent(json: String) {
self.sendEvent(withName: "RCTCrossPlatformEventBus.Event", body: json)
}
open override func supportedEvents() -> [String]! {
return ["RCTCrossPlatformEventBus.Event"]
}
open override func constantsToExport() -> [AnyHashable : Any]! {
return [:]
}
}
App_Bridging_Header.h
#ifndef ArchitectureDemo_Bridging_Header_h
#define ArchitectureDemo_Bridging_Header_h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
#import "RCTCrossPlatformEventBus.m"
Then, in Javascript (Typescript actually)
import { NativeModules, NativeEventEmitter } from 'react-native'
import { BehaviorSubject, Observable } from 'rxjs'
const { CrossPlatformEventBus } = NativeModules;
const eventEmitter = new NativeEventEmitter(CrossPlatformEventBus)
class RNCrossPlatformEventBus {
// we set up a private pipeline for events we can post to
private postableEventBus = new BehaviorSubject<string>('')
// and then expose it's observable for everyone to subscribe to
eventBus = this.postableEventBus.asObservable()
constructor() {
eventEmitter.addListener('RCTCrossPlatformEventBus.Event', (body) => {
this.processEventFromNative(body)
})
}
postEvent(json: string) {
this.postableEventBus.next(json)
CrossPlatformEventBus.processHybridEvent(json);
}
processEventFromNative(jsonString: string) {
this.postableEventBus.next(jsonString)
console.log(`React-Native received event from native ${jsonString}`)
}
}
export default new RNCrossPlatformEventBus()
I resolved my problem in the meantime and am posting the answer here for future reference.
As it turns out, initializing my RCTCrossPlatformEventBus - as I do in my swift file - Is an invalid operation. React-Native already initialize this, so rather than creating a singleton myself, I just had to override it's initializer like so:
open class RCTCrossPlatformEventBus: RCTEventEmitter {
public static var shared: RCTCrossPlatformEventBus?
override init() {
super.init()
RCTCrossPlatformEventBus.shared = self
}
...
This is my function in android :
#JavascriptInterface
public boolean GetMobileVersion() {
return true;
}
This is Calling function in JavaScript :
$(window).load(function () {
IsCallByMobileApp = false;
try {
IsCallByMobileApp = app.GetMobileVersion();/*Is call by android app*/
} catch (e) {
IsCallByMobileApp = false;
}
}
In iOS I am try to achieve same approach using WKWebView like this but it's not working:
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if(message.name == callbackhandler) {
webView.evaluateJavaScript("GetMobileVersion();") { (true, error) in
guard error == nil else {
print("there was an error")
return
}
print(Bool(true))
}
}
}
Can anyone tell me How to send true to Javascript function? Please give me answer in detail because I am new to iOS and Swift.
I have created a GitHub project for this. https://github.com/BKRApps/WKWebView-JS. check it out for more details.
Update the JavaScript:
function getMobileVersion(){
webkit.messageHandlers.VersionHandler.postMessage({})
}
function receivedMobileVersion(mobileVersion){
//here you will be getting the mobile version. Then execute the logic.
// i have added this only to cross check the version. you don't need to add this.
if(mobileVersion === true) {
webkit.messageHandlers.VerifyHandler.postMessage({version:mobileVersion})
}
}
getMobileVersion()
Add the below code to WKWebView Configuration:
configuration.userContentController.add(self, name: "VersionHandler")
// i have added this only to cross check the version. you don't need to add this.
configuration.userContentController.add(self, name: "VerifyHandler")
Update the WKScriptMessageHandler delegate:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "VersionHandler":
let mobileVersion = true //write the version logic and send the true or false.
let sendMobileVersionScript = "receivedMobileVersion(\(mobileVersion))"
self.wkWebView?.evaluateJavaScript("\(sendMobileVersionScript)", completionHandler: { (any, error) in
print("hello")
})
case "VerifyHandler":
print(message.body) // i have added this only to cross check the version. you
default:
break;
}
}
for more info : http://igomobile.de/2017/03/06/wkwebview-return-a-value-from-native-code-to-javascript/
Not sure if with WKWebView is the same as webview but you could try with plaintext as following:
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if(message.name == callbackhandler) {
webView.evaluateJavaScript("GetMobileVersion(true);") { (true, error) in
guard error == nil else {
print("there was an error")
return
}
print(Bool(true))
}
}
}
We use FileOpener2 plugin for cordova to open a downloaded .apk file from our servers. Recently, we found that Android 6.0 or higher devices are throwing an exception only on the file open process. We were able to trace this down to the cordova.js file, where the posted exception occurs. We have yet to find a cause or a fix, but have put a workaround in place. Any info would be amazing on this so we can maintain our in-app self updating process going on all Android devices.
Code (Working on Android <= 6.0):
// we need to access LocalFileSystem
window.requestFileSystem(LocalFileSystem.PERSISTENT, 5 * 1024 * 1024, function (fs) {
//Show user that download is occurring
$("#toast").dxToast({
message: "Downloading please wait..",
type: "warning",
visible: true,
displayTime: 20000
});
// we will save file in .. Download/OURAPPNAME.apk
var filePath = cordova.file.externalRootDirectory + '/Download/' + "OURAPPNAME.apk";
var fileTransfer = new FileTransfer();
var uri = encodeURI(appDownloadURL);
fileTransfer.download(uri, filePath, function (entry) {
//Show user that download is occurring/show user install is about to happen
$("#toast").dxToast({
message: "Download complete! Launching...",
type: "success",
visible: true,
displayTime: 2000
});
////Use pwlin's fileOpener2 plugin to let the system open the .apk
cordova.plugins.fileOpener2.open(
entry.toURL(),
'application/vnd.android.package-archive',
{
error: function (e) {
window.open(appDownloadURL, "_system");
},
success: function () { console.log('file opened successfully'); }
}
);
},
function (error) {
//Show user that download had an error
$("#toast").dxToast({
message: error.message,
type: "error",
displayTime: 5000
});
},
false);
})
Debugging Information:
THIS IS NOT OUR CODE, BUT APACHE/CORDOVA CODE
Problem File: cordova.js
function androidExec(success, fail, service, action, args) {
// argsJson - "["file:///storage/emulated/0/download/OURAPPNAME.apk","application/vnd.android.package-archive"]"
//callbackId - FileOpener21362683899
//action - open
//service FileOpener2
//bridgesecret - 1334209170
// msgs = "230 F09 FileOpener21362683899 sAttempt to invoke virtual method 'android.content.res.XmlResourceParser //android.content.pm.PackageItemInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference"
var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "#Null arguments.") {
androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
androidExec(success, fail, service, action, args);
androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
} else if (msgs) {
messagesFromNative.push(msgs);
// Always process async to avoid exceptions messing up stack.
nextTick(processMessages);
}
Is that possible create a function inside the WebView component, trigger React Native function?
It's possible but I'm not sure if it's the only way to do this.
Basically you can set an onNavigationStateChange event handler, and embed function call information in navigation url, here's an example of the concept.
In React Native context
render() {
return <WebView onNavigationStateChange={this._onURLChanged.bind(this)} />
}
_onURLChanged(e) {
// allow normal the natvigation
if(!e.url.startsWith('native://'))
return true
var payload = JSON.parse(e.url.replace('native://', ''))
switch(e.functionName) {
case 'toast' :
native_toast(e.data)
break
case 'camera' :
native_take_picture(e.data)
break
}
// return false to prevent webview navitate to the location of e.url
return false
}
To invoke native method, use this just trigger webview's navigation event and embed the function call information in URL.
window.location = 'native://' + JSON.stringify({
functionName : 'toast', data : 'show toast text'
})
use onMessage eventListner on <WebView/>
<WebView onMessage={onMessage} ... />
/** on message from webView -- window.ReactNativeWebView?.postMessage(data) */
const onMessage = event => {
const {
nativeEvent: {data},
} = event;
if (data === 'goBack') {
navigation.goBack();
} else if (data?.startsWith('navigate')) {
// navigate:::routeName:::stringifiedParams
try {
const [, routeName, params] = data.split(':::');
params = params ? JSON.parse(params) : {};
navigation.navigate(routeName, params);
} catch (err) {
console.log(err);
}
}
};
use this in your HTML to post message event
window.ReactNativeWebView?.postMessage("data")
You could inject a javascript function to the webview on load and then use onMessage to get response from the function you injected more info IN Here
yes it's possible , it existe a package for that react-native-webview-bridge.
I used it heavily in production and it works perfectly.
I am not sure, but my opinion is -
You can not. Webview can load only js part which we can define in Webview component. This is totally separate than other components, it is only just a viewable area.
I am creating a javascript xpcom component Its source is as follows-
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
function callback() { }
callback.prototype = {
classDescription: "My Hello World Javascript XPCOM Component",
classID: Components.ID("{3459D788-D284-4ef0-8AFF-96CBAF51BD35}"),
contractID: "#jscallback.p2psearch.com/f2f;1",
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.ICallback]),
received:function(data){
alert("Received: " +data);
},
status:function(data){
alert("Status: "+data);
},
expr:function(data){
alert("Expr: "+expr);
}
};
var components = [callback];
function NSGetModule(compMgr, fileSpec) {
return XPCOMUtils.generateModule(components);
}
When I call it using:
try {
const p2pjcid = "#jscallback.p2psearch.com/f2f;1";
var jobj = Components.classes[p2pjcid].createInstance();
jobj = jobj.QueryInterface(Components.interfaces.ICallback);
jobj.received("Hello from javascript")
}
catch (err) {
alert(err);
return;
}
I get Error as :
[Exception... "'[JavaScript Error: "alert is not defined" {file: "file:///C:/Documents%20and%20Settings/mypc/Application%20Data/Mozilla/Firefox/Profiles/ngi5btaf.default/extensions/spsarolkar#gmail/components/callback.js" line: 11}]' when calling method: [ICallback::received]" nsresult: "0x80570021 (NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS)" location: "JS frame :: chrome://sample/content/clock.js :: initClock :: line 28" data: yes]
That's because alert is not defined in XPCOM code (though you can get the component). It's not a good idea to alert in an extension as it blocks the user from interacting with the page(s) they are on. Use the non-modal toaster-box notification function:
const alert = Components.classes['#mozilla.org/alerts-service;1']
.getService(Components.interfaces.nsIAlertsService)
.showAlertNotification;
Use it in the following syntax: alert(icon, title, body).
Here's more info on MDC.
You can also use dump() from a JavaScript component.