WKWebview — сложная связь между Javascript и нативным кодом

В WKWebView мы можем вызывать код ObjectiveC/swift, используя обработчики сообщений webkit, например: webkit.messageHandlers.<handler>.pushMessage(message)

Он хорошо работает для простых функций javascript без параметров. Но;

  1. Можно ли вызвать собственный код с функцией обратного вызова JS в качестве параметров?
  2. Можно ли вернуть значение функции JS из собственного кода?

person Clement Prem    schedule 25.03.2015    source источник
comment
Для этой цели следует использовать PhoneGap (phonegap.com). Его кросс-платформа обеспечивает наиболее надежную связь между веб-просмотром и собственным кодом.   -  person atulkhatri    schedule 25.03.2015
comment
мое приложение использует некоторые сторонние skds для подключения к внешнему оборудованию, поэтому я не могу перейти на разрыв телефона. Я мог бы сделать это с помощью UIwebview + JCore, я ищу подобное решение.   -  person Clement Prem    schedule 26.03.2015
comment
Хорошо, тогда вам может понадобиться проверить это: github.com/marcuswestin/WebViewJavascriptBridge   -  person atulkhatri    schedule 26.03.2015
comment
В основном он взаимодействует с javascript на родной через UIWebviewDelegate методы; и с нативного на javascript через метод stringByEvaluatingJavaScriptFromString. Вы также можете создать свой собственный мост.   -  person atulkhatri    schedule 26.03.2015
comment
Я пытаюсь решить ту же проблему без каких-либо сторонних библиотек. Я попытался сохранить обратный вызов в глобальном словаре в JS с идентификатором запроса ключа. Собственный код обращается к webView.evaluateJavaScript (идентификатор запроса). Это работало в некоторых случаях, но не во всех. Возможно, потому что глобальная переменная, вероятно, для каждого кадра. Все еще исследую это.   -  person Feru    schedule 12.03.2016
comment
@Feru, ты нашел решение?   -  person Crashalot    schedule 25.10.2016
comment
Не могли бы вы превратить объект Swift в строку JSON и передать эту строку в JavaScript (который затем декодирует JSON)?   -  person Crashalot    schedule 25.10.2016


Ответы (7)


К сожалению, я не смог найти родное решение.

Но следующий обходной путь решил мою проблему

Используйте обещания javascript, и вы можете вызвать функцию разрешения из своего кода iOS.

ОБНОВЛЕНИЕ

Вот как вы можете использовать обещание

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

в iOS

Вызовите функцию window.onMessageReceive с соответствующим идентификатором обработчика

person Clement Prem    schedule 19.05.2015
comment
Пример? Использовать обещания Javascript не очень понятно. - person datWooWoo; 25.05.2016
comment
Я думаю использовать либо wkwebview, либо crosswalkwebview для возврата значения из метода obj c, можете ли вы предоставить код ios - person skyshine; 15.07.2016
comment
Спасибо друг! Думаю про промисы, но при маленьком опыте в JS - не смог реализовать. Ваш фрагмент помогает мне понять это. - person fir; 24.08.2016
comment
@ClementPrem вам удалось заставить его работать с таким подходом? У меня проблемы с пониманием (я не гуру JS). Как добавить новые обработчики? Просто закодировать их в this.handlers? Являются ли обработчики функциями? Правилен ли синтаксис в строке 3? (=›) Кто вызывает sendMessage(data)? - person iOSAddicted; 10.03.2017
comment
Чтобы сделать это синхронно, работает этот ответ. - person Sasuke Uchiha; 25.03.2018

Есть способ вернуть JS возвращаемое значение из нативного кода с помощью WkWebView. Это небольшой хак, но у меня он работает без проблем, и наше рабочее приложение использует много JS/Native-коммуникаций.

В WKUiDelegate, назначенном WKWebView, переопределите RunJavaScriptTextInputPanel. Для этого используется способ, которым делегат обрабатывает функцию подсказки JS:

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

        }
    }

В моем случае я передаю data type=xyz,name=xyz,data=xyz для передачи аргументов. Мой код ToUiSynch() обрабатывает запрос и всегда возвращает строку, которая возвращается в JS как простое возвращаемое значение. .

В JS я просто вызываю функцию prompt() с отформатированной строкой аргументов и получаю возвращаемое значение:

return prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
person Nathan Brown    schedule 27.09.2016
comment
Ницца! Я переношу проект с помощью webViews с Android (где собственные вызовы могут возвращать значения) на iOS. Это сэкономило мне немало времени и изменений кода. Спасибо, что поделились, сэр. - person cviejo; 06.10.2016
comment
Вы знаете, как это сделать в Objective-c? - person user3098173; 04.09.2017
comment
В WKUiDelegate нет такой функции для переопределения. - person Sasuke Uchiha; 24.03.2018
comment
Это коварно умная идея; Спасибо, что поделился. - person jeff-h; 17.06.2018
comment
Внимание, этот метод нестабилен! Если большое количество сообщений отправляется очень быстро, WKWebView аварийно завершает работу (становится черным) или перезагружается. - person Seven Systems; 14.11.2019
comment
@ Натан Браун, ты используешь jQuery? Если да, то как вы решаете проблемы с прерыванием установки значений для html-элементов. В случаях, когда элементы html устанавливаются до вызова подсказки? Спасибо - person Ievgen; 09.05.2020

В этом ответе используется идея из ответа Натана Брауна выше.

Насколько мне известно, в настоящее время нет способа вернуть данные обратно в javascript синхронно. Надеюсь, Apple предоставит решение в будущем выпуске.

Таким образом, хак должен перехватывать быстрые вызовы от js. Apple предоставила эту функциональность, чтобы показать собственный дизайн всплывающего окна, когда js вызывает предупреждение, подсказку и т. д. Теперь, поскольку подсказка — это функция, в которой вы показываете данные пользователю (мы будем использовать это как параметр метода) и ответ от пользователя на это приглашение будет возвращено обратно в js (мы будем использовать это как возвращаемые данные)

Может быть возвращена только строка. Это происходит синхронно.

Мы можем реализовать вышеуказанную идею следующим образом:

В конце javascript: вызовите метод swift следующим образом:

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

В swift/xcode end сделайте следующее:

  1. Реализуйте протокол WKUIDelegate, а затем назначьте реализацию свойству WKWebviews uiDelegate следующим образом:

    self.webView.uiDelegate = self
    
  2. Теперь напишите этот func webView, чтобы переопределить (?)/перехватить запрос на prompt из 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)
    }}
    

Если вы не вызовете completionHandler(), выполнение js не продолжится. Теперь проанализируйте json и вызовите соответствующий быстрый метод.

    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
}
person Sasuke Uchiha    schedule 25.03.2018
comment
У меня есть код, который должен выполняться перед вызовом подсказки, например. document.getElementId('hint').innerHTML = "hello world"; return prompt("send message to native"); и переопределите для RunJavaScripttextInputPanel(...) в swift, как упоминалось здесь. Код перед подсказкой никогда не выполняется; приглашение просто выполняется первым, и после возврата из исходного кода строки выше приглашения (например, document.getElementId('hint').innerHTML = "hello world";) больше никогда не выполняются. - person Ievgen; 08.05.2020
comment
Я думаю, что метод приглашения javascript определенно находится на правильном пути. Однако я попытался реализовать его и заметил, что он сработал только в первый раз (страница сделает полтора десятка таких запросов). Кто-нибудь видел подобную проблему раньше? - person JMiao; 10.12.2020

XWebView в настоящее время является лучшим выбором. Он может автоматически отображать нативные объекты в среде javascript.

Для вопроса 2 вы должны передать функцию обратного вызова JS в натив, чтобы получить результат, потому что синхронизированная связь между JS и нативом невозможна.

Дополнительные сведения см. в примере.

person soflare    schedule 28.04.2015
comment
Сохранит ли это контекст области Javascript? Например, если я передам var a = 2; var cb = function() { console.warn(a, arguments); }; ios.doSomething(cb);, будет ли в журнале 2? - person Bartłomiej Zalewski; 15.02.2016
comment
К сожалению нет. Собственный код может оценивать функции обратного вызова только в глобальной области видимости. Обходной путь: var a = 2; var cb = функция (а) { console.warn (а, аргументы); }; ios.doSomething(cb.bind(это, а)); - person soflare; 18.02.2016

У меня есть обходной путь для вопроса 1.

PostMessage с JavaScript

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

Управляйте этим в своем проекте 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);
        }
    }];
}
person 张建伟    schedule 24.02.2017

Мне удалось решить эту проблему — добиться двусторонней связи между нативным приложением и WebView (JS) — используя postMessage в JS и evaluateJavaScript в нативном коде.

Решение от высокого уровня было:

  • WebView (JS) code:
    • Create a general function to get data from Native (I called it getDataFromNative for Native, which calls another callback function (I called it callbackForNative), which can be reassigned
    • When wanting to call Native with some data and requiring a response, do the following:
      • Reassign callbackForNative to whatever function you want
      • Вызовите Native из WebView, используя postMessage
  • Native code:
    • Use the userContentController to listen to incoming messages from the WebView (JS)
    • Используйте evaluateJavaScript для вызова функции getDataFromNative JS с любыми параметрами, которые вы хотите

Вот код:

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

Нативный (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. Я фронтенд-разработчик, поэтому хорошо разбираюсь в JS, но у меня очень мало опыта в Swift.

P.S.2 Убедитесь, что ваш WebView не кэширован, иначе вы можете расстроиться, когда WebView не изменится, несмотря на изменения в HTML/CSS/JS.

Рекомендации:

Мне очень помогло это руководство: https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b503

person Liran H    schedule 27.05.2019

Вы не можете. Как упоминалось в @Clement, вы можете использовать обещания и вызывать функцию разрешения. Неплохой (хотя и с использованием Deferred, который сейчас считается антишаблоном) пример GoldenGate.

В Javascript вы можете создать объект двумя способами: отправить и разрешить: (я скомпилировал cs в js для облегчения чтения)

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;

})();

Затем вы звоните

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

И со стороны 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)
    }

Обратите внимание на эту замечательную статью о обещаниях.

person Karol Klepacki    schedule 27.05.2015
comment
Не используйте случайным образом переменные в своем ответе, абсолютно не объясняя, что это такое: см. bridge.run(plugin, method, args) - person datWooWoo; 25.05.2016