WKWebview - Complex communication between Javascript & native code

后端 未结 7 2061
渐次进展
渐次进展 2020-12-12 17:39

In WKWebView we can call ObjectiveC/swift code using webkit message handlers eg: webkit.messageHandlers..pushMessage(message)

It works we

相关标签:
7条回答
  • 2020-12-12 17:55

    I have a workaround for question1.

    PostMessage with JavaScript

    window.webkit.messageHandlers.<handler>.postMessage(function(data){alert(data);}+"");
    

    Handle It in your Objective-C project

    -(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);
            }
        }];
    }
    
    0 讨论(0)
  • 2020-12-12 18:01

    Unfortunately I couldn't find a native solution.

    But the following workaround solved my problem

    Use javascript promises & you can call the resolve function from your iOS code.

    UPDATE

    This is how you can use promise

    In 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});
        });
      }
    

    in iOS

    Call the window.onMessageReceive function with appropriate handler id

    0 讨论(0)
  • 2020-12-12 18:02

    There is a way to get a return value back to JS from the native code using WkWebView. It is a little hack but works fine for me without problems, and our production app uses a lot of JS/Native communication.

    In the WKUiDelegate assigned to the WKWebView, override the RunJavaScriptTextInputPanel. This uses the way that the delegate handles the JS prompt function to accomplish this:

        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.");
    
            }
        }
    

    In my case, I am passing data type=xyz,name=xyz,data=xyz to pass the args in. My ToUiSynch() code handles the request and always returns a string, which goes back to the JS as a simple return value.

    In the JS, I am simply calling the prompt() function with my formatted args string and getting a return value:

    return prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
    
    0 讨论(0)
  • 2020-12-12 18:05

    You can't. As @Clement mentioned, you can use promises and call the resolve function. Quite good (although using Deferred - which is considered to be anti-pattern now) example is GoldenGate.

    In Javascript you can create object with two methods: dispatch and resolve: (I've compiled cs to js for easier reading)

    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;
    
    })();
    

    Then you call

      Goldengate.dispatch("ReadLater", "makeSomethingHappen", []);
    

    And from the iOS side:

        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)
        }
    

    Please consider this great article about promises

    0 讨论(0)
  • 2020-12-12 18:07

    This answer uses the idea from Nathan Brown's answer above.

    As far as I know, currently there is no way to return data back to javascript synchronous way. Hopefully apple will provide the solution in future release.

    So hack is to intercept the prompt calls from js. Apple provided this functionality in order to show native popup design when js calls the alert, prompt etc. Now since prompt is the feature, where you show the data to user (we will exploit this as method param ) and the response from user to this prompt will be returned back to js (we'll exploit this as return data)

    Only string can be returned. This happens in synchronous way.

    We can implement the above idea as follows:

    At the javascript end: call the swift method in the following way:

        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');
        }
    }
    

    At the swift/xcode end do as follows:

    1. Implement the protocol WKUIDelegate and then assign the implementation to WKWebviews uiDelegate property like this:

      self.webView.uiDelegate = self
      
    2. Now write this func webView to override (?) / intercept the request for prompt from 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)
      }}
      

    If you don't call the completionHandler() then js execution will not proceed. Now parse the json and call appropriate swift method.

        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
    }
    
    0 讨论(0)
  • 2020-12-12 18:09

    XWebView is the best choice currently. It can automatically expose native objects to javascript environment.

    For the question 2, you have to pass an JS callback function to native to get result, because synchronized communication from JS to native is impossible.

    For More details, check the sample app.

    0 讨论(0)
提交回复
热议问题