Call JavaScript function from native code in WKWebView Call JavaScript function from native code in WKWebView ios ios

Call JavaScript function from native code in WKWebView


(I filed a Radar for this shortly after asking the question here.)

A new method was just added a few days ago (thanks jcesarmobile for pointing it out):

Add -[WKWebView evaluateJavaScript:completionHandler:]
http://trac.webkit.org/changeset/169765

The method is available in iOS 8 beta 3 and up. Here's the new method signature:

/* @abstract Evaluates the given JavaScript string.  @param javaScriptString The JavaScript string to evaluate.  @param completionHandler A block to invoke when script evaluation completes     or fails.  @discussion The completionHandler is passed the result of the script evaluation     or an error. */ - (void)evaluateJavaScript:(NSString *)javaScriptString         completionHandler:(void (^)(id, NSError *))completionHandler; 

Docs are available here: https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript.


Details

  • Xcode 9.1, Swift 4
  • Xcode 10.2 (10E125), Swift 5

Description

The script is inserted into page which will displayed in WKWebView. This script will return the page URL (but you can write another JavaScript code). This means that the script event is generated on the web page, but it will be handled in our function:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {...}

Solution

extension WKUserScript {    enum Defined: String {        case getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"        case getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"        var name: String { return rawValue }        private var injectionTime: WKUserScriptInjectionTime {            switch self {                case .getUrlAtDocumentStartScript: return .atDocumentStart                case .getUrlAtDocumentEndScript: return .atDocumentEnd            }        }        private var forMainFrameOnly: Bool {            switch self {                case .getUrlAtDocumentStartScript: return false                case .getUrlAtDocumentEndScript: return false            }        }        private var source: String {            switch self {            case .getUrlAtDocumentEndScript, .getUrlAtDocumentStartScript:                return "webkit.messageHandlers.\(name).postMessage(document.URL)"            }        }        fileprivate func create() -> WKUserScript {            return WKUserScript(source: source,                                injectionTime: injectionTime,                                forMainFrameOnly: forMainFrameOnly)        }    }}extension WKWebViewConfiguration {    func add(script: WKUserScript.Defined, scriptMessageHandler: WKScriptMessageHandler) {        userContentController.addUserScript(script.create())        userContentController.add(scriptMessageHandler, name: script.name)    }}

Usage

Init WKWebView

 let config = WKWebViewConfiguration() config.add(script: .getUrlAtDocumentStartScript, scriptMessageHandler: self) config.add(script: .getUrlAtDocumentEndScript, scriptMessageHandler: self) webView = WKWebView(frame:  UIScreen.main.bounds, configuration: config) webView.navigationDelegate = self view.addSubview(webView)

Catch events

extension ViewController: WKScriptMessageHandler {    func userContentController (_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {        if  let script = WKUserScript.Defined(rawValue: message.name),            let url = message.webView?.url {                switch script {                    case .getUrlAtDocumentStartScript: print("start: \(url)")                    case .getUrlAtDocumentEndScript: print("end: \(url)")                }        }    }}

Full Code example

import UIKitimport WebKitclass ViewController: UIViewController, WKNavigationDelegate {    private var webView = WKWebView()    override func viewDidLoad() {        super.viewDidLoad()        let config = WKWebViewConfiguration()        config.add(script: .getUrlAtDocumentStartScript, scriptMessageHandler: self)        config.add(script: .getUrlAtDocumentEndScript, scriptMessageHandler: self)        webView = WKWebView(frame:  UIScreen.main.bounds, configuration: config)        webView.navigationDelegate = self        view.addSubview(webView)    }    override func viewDidAppear(_ animated: Bool) {        super.viewDidAppear(animated)        webView.load(urlString: "http://apple.com")    }}extension ViewController: WKScriptMessageHandler {    func userContentController (_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {        if  let script = WKUserScript.Defined(rawValue: message.name),            let url = message.webView?.url {                switch script {                    case .getUrlAtDocumentStartScript: print("start: \(url)")                    case .getUrlAtDocumentEndScript: print("end: \(url)")                }        }    }}extension WKWebView {    func load(urlString: String) {        if let url = URL(string: urlString) {            load(URLRequest(url: url))        }    }}extension WKUserScript {    enum Defined: String {        case getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"        case getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"        var name: String { return rawValue }        private var injectionTime: WKUserScriptInjectionTime {            switch self {                case .getUrlAtDocumentStartScript: return .atDocumentStart                case .getUrlAtDocumentEndScript: return .atDocumentEnd            }        }        private var forMainFrameOnly: Bool {            switch self {                case .getUrlAtDocumentStartScript: return false                case .getUrlAtDocumentEndScript: return false            }        }        private var source: String {            switch self {            case .getUrlAtDocumentEndScript, .getUrlAtDocumentStartScript:                return "webkit.messageHandlers.\(name).postMessage(document.URL)"            }        }        fileprivate func create() -> WKUserScript {            return WKUserScript(source: source,                                injectionTime: injectionTime,                                forMainFrameOnly: forMainFrameOnly)        }    }}extension WKWebViewConfiguration {    func add(script: WKUserScript.Defined, scriptMessageHandler: WKScriptMessageHandler) {        userContentController.addUserScript(script.create())        userContentController.add(scriptMessageHandler, name: script.name)    }}

Info.plist

add in your Info.plist transport security setting

<key>NSAppTransportSecurity</key><dict>    <key>NSAllowsArbitraryLoads</key>    <true/></dict>

Result

enter image description here

Resources

Document Object Properties and Methods


It may not be an ideal method but depending on your use-case, you can just reload the WKWebView after you've infected the user script:

NSString *scriptSource = @"alert('WKWebView JS Call!')";WKUserScript *userScript = [[WKUserScript alloc] initWithSource:scriptSource                                                  injectionTime:WKUserScriptInjectionTimeAtDocumentEnd                                               forMainFrameOnly:YES];[wkWebView.configuration.userContentController addUserScript:userScript];[wkWebView reload];