web-dev-qa-db-fra.com

WKWebview - Communication complexe entre Javascript et code natif

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;

  1. Est-il possible d'appeler du code natif avec la fonction de rappel JS comme paramètres?
  2. Est-il possible de renvoyer une valeur à la fonction JS à partir du code natif?
24
Clement Prem

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é

10
Clement Prem

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 ));
9
Nathan Brown

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 .

3
soflare

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:

  1. Implémentez le protocole WKUIDelegate, puis affectez l'implémentation à la propriété WKWebviews uiDelegate comme ceci:

    self.webView.uiDelegate = self
    
  2. 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
}
3
Sasuke Uchiha

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);
        }
    }];
}
1
张建伟

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:

  • Code WebView (JS):
    • Créez une fonction générale pour obtenir des données de Native (je l'ai appelée getDataFromNative pour Native, qui appelle une autre fonction de rappel (je l'ai appelée callbackForNative), qui peut être réaffectée
    • Lorsque vous souhaitez appeler Native avec certaines données et nécessiter une réponse, procédez comme suit:
      • Réaffectez callbackForNative à la fonction souhaitée
      • Appelez Native depuis WebView en utilisant postMessage
  • Code natif:
    • Utilisez le userContentController pour écouter les messages entrants de WebView (JS)
    • Utilisez evaluateJavaScript pour appeler votre fonction getDataFromNative JS avec les paramètres de votre choix

Voici 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

0
Liran H