WKWebview-Comunicación compleja entre Javascript y código nativo
ios objective-c (6)
Desafortunadamente no pude encontrar una solución nativa.
Pero la siguiente solución solucionó mi problema
Use las promesas de javascript y puede llamar a la función de resolución desde su código iOS.
ACTUALIZAR
Así es como puedes usar la promesa.
En js
this.id = 1;
this.handlers = {};
window.onMessageReceive = (handle, error, data) => {
if (error){
this.handlers[handle].resolve(data);
}else{
this.handlers[handle].reject(data);
}
delete this.handlers[handle];
};
}
sendMessage(data) {
return new Promise((resolve, reject) => {
const handle = ''m''+ this.id++;
this.handlers[handle] = { resolve, reject};
window.webkit.messageHandlers.<yourHandler>.postMessage({data: data, id: handle});
});
}
en iOS
Llame a la función window.onMessageReceive
con el identificador de controlador adecuado
En WKWebView podemos llamar al código ObjectiveC / swift utilizando los controladores de mensajes de webkit, por ejemplo: webkit.messageHandlers.<handler>.pushMessage(message)
Funciona bien para funciones simples de javascript sin parámetros. Pero;
- ¿Es posible llamar código nativo con la función de devolución de llamada JS como parámetros?
- ¿Es posible devolver un valor a la función JS desde el código nativo?
Esta respuesta utiliza la idea de la answer de Nathan Brown anterior.
Por lo que sé, actualmente no hay forma de devolver los datos a javascript de forma síncrona . Esperemos que Apple proporcione la solución en futuras versiones.
Así que hackear es interceptar las llamadas de js. Apple proporcionó esta funcionalidad para mostrar un diseño emergente nativo cuando js llama la alerta, el indicador, etc. Ahora, desde la solicitud es la característica, donde le muestra los datos al usuario (explotaremos esto como método param) y la respuesta del usuario a este el mensaje se devolverá a js (lo explotaremos como datos de retorno)
Sólo se puede devolver la cadena. Esto sucede de manera síncrona.
Podemos implementar la idea anterior de la siguiente manera:
En el extremo de javascript: llame al método swift de la siguiente manera:
function callNativeApp(){
console.log("callNativeApp called");
try {
//webkit.messageHandlers.callAppMethodOne.postMessage("Hello from JavaScript");
var type = "SJbridge";
var name = "functionOne";
var data = {name:"abc", role : "dev"}
var payload = {type: type, functionName: name, data: data};
var res = prompt(JSON.stringify (payload));
//{"type":"SJbridge","functionName":"functionOne","data":{"name":"abc","role":"dev"}}
//res is the response from swift method.
} catch(err) {
console.log(''The native context does not exist yet'');
}
}
En el final de swift / xcode haga lo siguiente:
Implemente el protocolo
WKUIDelegate
y luego asigne la implementación a la propiedad uiDelegate deuiDelegate
esta manera:self.webView.uiDelegate = self
Ahora escriba esta
func webView
para anular (?) / Interceptar la solicitud deprompt
de javascript.func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { if let dataFromString = prompt.data(using: .utf8, allowLossyConversion: false) { let payload = JSON(data: dataFromString) let type = payload["type"].string! if (type == "SJbridge") { let result = callSwiftMethod(prompt: payload) completionHandler(result) } else { AppConstants.log("jsi_", "unhandled prompt") completionHandler(defaultText) } }else { AppConstants.log("jsi_", "unhandled prompt") completionHandler(defaultText) }}
Si no llama a completionHandler()
, la ejecución de js no continuará. Ahora analiza el json y llama al método swift apropiado.
func callSwiftMethod(prompt : JSON) -> String{
let functionName = prompt["functionName"].string!
let param = prompt["data"]
var returnValue = "returnvalue"
AppConstants.log("jsi_", "functionName: /(functionName) param: /(param)")
switch functionName {
case "functionOne":
returnValue = handleFunctionOne(param: param)
case "functionTwo":
returnValue = handleFunctionTwo(param: param)
default:
returnValue = "returnvalue";
}
return returnValue
}
Hay una forma de obtener un valor de retorno a JS a partir del código nativo utilizando WkWebView. Es un pequeño truco pero funciona bien para mí sin problemas, y nuestra aplicación de producción usa mucha comunicación JS / Native.
En el WKUiDelegate asignado a WKWebView, anule el RunJavaScriptTextInputPanel. Esto utiliza la forma en que el delegado maneja la función de solicitud de JS para lograr esto:
public override void RunJavaScriptTextInputPanel (WebKit.WKWebView webView, string prompt, string defaultText, WebKit.WKFrameInfo frame, Action<string> completionHandler)
{
// this is used to pass synchronous messages to the ui (instead of the script handler). This is because the script
// handler cannot return a value...
if (prompt.StartsWith ("type=", StringComparison.CurrentCultureIgnoreCase)) {
string result = ToUiSynch (prompt);
completionHandler.Invoke ((result == null) ? "" : result);
} else {
// actually run an input panel
base.RunJavaScriptTextInputPanel (webView, prompt, defaultText, frame, completionHandler);
//MobApp.DisplayAlert ("EXCEPTION", "Input panel not implemented.");
}
}
En mi caso, estoy pasando el tipo de datos = xyz, nombre = xyz, datos = xyz para pasar los argumentos. Mi código ToUiSynch () maneja la solicitud y siempre devuelve una cadena, que regresa a la JS como un simple valor de retorno .
En el JS, simplemente estoy llamando a la función prompt () con mi cadena args formateada y obteniendo un valor de retorno:
return prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
Tengo una solución para la pregunta 1.
PostMessage con JavaScript
window.webkit.messageHandlers.<handler>.postMessage(function(data){alert(data);}+"");
Manéjalo en tu proyecto Objective-C
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSString *callBackString = message.body;
callBackString = [@"(" stringByAppendingString:callBackString];
callBackString = [callBackString stringByAppendingFormat:@")(''%@'');", @"Some RetString"];
[message.webView evaluateJavaScript:callBackString completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
if (error) {
NSLog(@"name = %@ error = %@",@"", error.localizedDescription);
}
}];
}
Usted no puede Como mencionó @Clement, puede usar promesas y llamar a la función de resolución. Bastante bien (aunque se usa el término Aplazado, que ahora se considera antipatrón) es GoldenGate .
En Javascript puede crear objetos con dos métodos: enviar y resolver: (He compilado cs a js para facilitar la lectura)
this.Goldengate = (function() {
function Goldengate() {}
Goldengate._messageCount = 0;
Goldengate._callbackDeferreds = {};
Goldengate.dispatch = function(plugin, method, args) {
var callbackID, d, message;
callbackID = this._messageCount;
message = {
plugin: plugin,
method: method,
"arguments": args,
callbackID: callbackID
};
window.webkit.messageHandlers.goldengate.postMessage(message);
this._messageCount++;
d = new Deferred;
this._callbackDeferreds[callbackID] = d;
return d.promise;
};
Goldengate.callBack = function(callbackID, isSuccess, valueOrReason) {
var d;
d = this._callbackDeferreds[callbackID];
if (isSuccess) {
d.resolve(valueOrReason[0]);
} else {
d.reject(valueOrReason[0]);
}
return delete this._callbackDeferreds[callbackID];
};
return Goldengate;
})();
Entonces llamas
Goldengate.dispatch("ReadLater", "makeSomethingHappen", []);
Y desde el lado de iOS:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
let message = message.body as! NSDictionary
let plugin = message["plugin"] as! String
let method = message["method"] as! String
let args = transformArguments(message["arguments"] as! [AnyObject])
let callbackID = message["callbackID"] as! Int
println("Received message #/(callbackID) to dispatch /(plugin)./(method)(/(args))")
run(plugin, method, args, callbackID: callbackID)
}
func transformArguments(args: [AnyObject]) -> [AnyObject!] {
return args.map { arg in
if arg is NSNull {
return nil
} else {
return arg
}
}
}
func run(plugin: String, _ method: String, _ args: [AnyObject!], callbackID: Int) {
if let result = bridge.run(plugin, method, args) {
println(result)
switch result {
case .None: break
case .Value(let value):
callBack(callbackID, success: true, reasonOrValue: value)
case .Promise(let promise):
promise.onResolved = { value in
self.callBack(callbackID, success: true, reasonOrValue: value)
println("Promise has resolved with value: /(value)")
}
promise.onRejected = { reason in
self.callBack(callbackID, success: false, reasonOrValue: reason)
println("Promise was rejected with reason: /(reason)")
}
}
} else {
println("Error: No such plugin or method")
}
}
private func callBack(callbackID: Int, success: Bool, reasonOrValue: AnyObject!) {
// we''re wrapping reason/value in array, because NSJSONSerialization won''t serialize scalar values. to be fixed.
bridge.vc.webView.evaluateJavaScript("Goldengate.callBack(/(callbackID), /(success), /(Goldengate.toJSON([reasonOrValue])))", completionHandler: nil)
}
Por favor considera este gran artículo sobre promesas.
XWebView es la mejor opción actualmente. Puede exponer automáticamente los objetos nativos al entorno javascript.
Para la pregunta 2, debe pasar una función de devolución de llamada JS a nativa para obtener un resultado, porque la comunicación sincronizada de JS a nativa es imposible.
Para más detalles, consulte la aplicación de sample .