Dans WKWebView, nous pouvons appeler le code ObjectiveC/Swift en utilisant des gestionnaires de messages webkit, par exemple: webkit.messageHandlers.<handler>.pushMessage(message)
Cela fonctionne bien pour les fonctions javascript simples sans paramètres. Mais;
Malheureusement, je n'ai pas pu trouver de solution native.
Mais la solution de contournement suivante a résolu mon problème
Utilisez des promesses javascript et vous pouvez appeler la fonction de résolution à partir de votre code iOS.
MISE À JOUR
Voilà comment vous pouvez utiliser la promesse
Dans 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});
});
}
dans iOS
Appeler le window.onMessageReceive
fonction avec l'ID de gestionnaire approprié
Il existe un moyen de récupérer une valeur de retour vers JS à partir du code natif à l'aide de WkWebView. C'est un peu hack mais ça marche bien pour moi sans problème, et notre application de production utilise beaucoup de communication JS/Native.
Dans le WKUiDelegate affecté au WKWebView, remplacez le RunJavaScriptTextInputPanel. Cela utilise la façon dont le délégué gère la fonction d'invite JS pour accomplir cela:
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.");
}
}
Dans mon cas, je transmets le type de données = xyz, nom = xyz, data = xyz pour transmettre les arguments. Mon code ToUiSynch () gère la demande et renvoie toujours une chaîne, qui retourne au JS comme une simple valeur de retour .
Dans le JS, j'appelle simplement la fonction Prompt () avec ma chaîne args formatée et j'obtiens une valeur de retour:
return Prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
XWebView est le meilleur choix actuellement. Il peut automatiquement exposer des objets natifs à l'environnement javascript.
Pour la question 2, vous devez passer une fonction de rappel JS à native pour obtenir le résultat, car la communication synchronisée de JS à native est impossible.
Pour plus de détails, consultez l'application exemple .
Cette réponse utilise l'idée de Nathan Brown réponse ci-dessus.
Pour autant que je sache, il n'existe actuellement aucun moyen de renvoyer les données en mode javascript synchrone . Espérons que Apple fournira la solution dans la prochaine version.
Le hack consiste donc à intercepter les appels rapides de js. Apple a fourni cette fonctionnalité afin d'afficher la conception native de popup lorsque js appelle l'alerte, l'invite, etc. param) et la réponse de l'utilisateur à cette invite sera retournée à js (nous l'exploiterons comme données de retour)
Seule la chaîne peut être retournée. Cela se produit de manière synchrone.
Nous pouvons implémenter l'idée ci-dessus comme suit:
À la fin javascript: appelez la méthode Swift de la manière suivante:
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');
}
}
À la fin Swift/xcode , procédez comme suit:
Implémentez le protocole WKUIDelegate
, puis affectez l'implémentation à la propriété WKWebviews uiDelegate
comme ceci:
self.webView.uiDelegate = self
Maintenant, écrivez ce func webView
Pour remplacer (?)/Intercepter la demande de Prompt
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 vous n'appelez pas la completionHandler()
, l'exécution de js ne se poursuivra pas. Maintenant, analysez le json et appelez la méthode appropriée Swift.
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
}
J'ai une solution de contournement pour la question 1.
PostMessage avec JavaScript
window.webkit.messageHandlers.<handler>.postMessage(function(data){alert(data);}+"");
Manipulez-le dans votre projet 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);
}
}];
}
J'ai réussi à résoudre ce problème - pour établir une communication bidirectionnelle entre l'application native et WebView (JS) - en utilisant postMessage
dans le JS et evaluateJavaScript
dans le code natif.
La solution de haut niveau était:
getDataFromNative
pour Native, qui appelle une autre fonction de rappel (je l'ai appelée callbackForNative
), qui peut être réaffectéecallbackForNative
à la fonction souhaitéepostMessage
userContentController
pour écouter les messages entrants de WebView (JS)evaluateJavaScript
pour appeler votre fonction getDataFromNative
JS avec les paramètres de votre choixVoici le code:
JS:
// Function to get data from Native
window.getDataFromNative = function(data) {
window.callbackForNative(data)
}
// Empty callback function, which can be reassigned later
window.callbackForNative = function(data) {}
// Somewhere in your code where you want to send data to the native app and have it call a JS callback with some data:
window.callbackForNative = function(data) {
// Do your stuff here with the data returned from the native app
}
webkit.messageHandlers.YOUR_NATIVE_METHOD_NAME.postMessage({ someProp: 'some value' })
Natif (Swift):
// Call this function from `viewDidLoad()`
private func setupWebView() {
let contentController = WKUserContentController()
contentController.add(self, name: "YOUR_NATIVE_METHOD_NAME")
// You can add more methods here, e.g.
// contentController.add(self, name: "onComplete")
let config = WKWebViewConfiguration()
config.userContentController = contentController
self.webView = WKWebView(frame: self.view.bounds, configuration: config)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("Received message from JS")
if message.name == "YOUR_NATIVE_METHOD_NAME" {
print("Message from webView: \(message.body)")
sendToJavaScript(params: [
"foo": "bar"
])
}
// You can add more handlers here, e.g.
// if message.name == "onComplete" {
// print("Message from webView from onComplete: \(message.body)")
// }
}
func sendToJavaScript(params: JSONDictionary) {
print("Sending data back to JS")
let paramsAsString = asString(jsonDictionary: params)
self.webView.evaluateJavaScript("getDataFromNative(\(paramsAsString))", completionHandler: nil)
}
func asString(jsonDictionary: JSONDictionary) -> String {
do {
let data = try JSONSerialization.data(withJSONObject: jsonDictionary, options: .prettyPrinted)
return String(data: data, encoding: String.Encoding.utf8) ?? ""
} catch {
return ""
}
}
P.S. Je suis développeur front-end, donc je suis très compétent en JS, mais j'ai très peu d'expérience en Swift.
P.S.2 Assurez-vous que votre WebView n'est pas mis en cache, ou vous pourriez être frustré lorsque le WebView ne change pas malgré les modifications apportées à HTML/CSS/JS.
Les références:
Ce guide m'a beaucoup aidé: https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b5