Call JavaScript function from native code in WKWebView
Asked Answered
U

7

62

Using a WKWebView in iOS 8, how can I run a JavaScript function from the native side or otherwise communicate from the native side to JavaScript? There doesn't appear to be a method akin to UIWebView's stringByEvaluatingJavaScriptFromString:.

(I can use - addScriptMessageHandler:name: on the configuration.userContentController object to allow communication from JS to native, but I'm looking for the opposite direction.)

Uncharted answered 4/6, 2014 at 23:37 Comment(8)
One failed attempt: I tried doing webView.loadRequest(NSURLRequest(URL: NSURL(string: "javascript:alert(1)"))) but nothing seemed to happen.Uncharted
I filed a radar for this -- marked as duplicate: openradar.me/radar?id=6415485126049792Uncharted
File it here as a feature request bugreport.apple.comSullen
@Sullen That's exactly what I did; see my previous comment.Uncharted
You just said you filed a radar and apple don't read the radars, that's why I told you to file it on bugreport apple siteSullen
As I understand it, reports on bugreport.apple.com are turned into radars. As I mentioned, Apple already responded to the report and marked it as a duplicate of an earlier bug.Uncharted
It seem that they fixed trac.webkit.org/changeset/169765Sullen
See also devforums.apple.com/message/986573.Violetavioletta
U
77

(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.

Uncharted answered 14/6, 2014 at 23:15 Comment(1)
You may also need to enable Javascript in your WKWebView (seems to be disabled by default now). let preferences = WKPreferences() preferences.javaScriptEnabled = true let configuration = WKWebViewConfiguration() configuration.preferences = preferences webView = WKWebView(frame: .zero, configuration: configuration)Franza
F
27

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 UIKit
import WebKit

class 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

Fractious answered 21/11, 2016 at 22:13 Comment(4)
Just an FYI addScript is not (anymore?) a method on WKWebViewConfiguration.Gloriole
I want to add username and password in my webview url so that user automatically login in . my app can you please suggest it how I can use your code.Repartee
I do not know your authorisation mechanism, but usually people use Alamofire library or UrlSession. P.S. Did you try to load webView.loadUrl(string: "YOUR_URL_WITH_LOGIN_AND_PASSWORD")Fractious
instead of using config.add, I used webview.configuration.userContentController.add(self, name: "handlernamehere") Also, make sure to enable javascript: webview.configuration.preferences.javaScriptEnabled = trueSuperorganic
S
7

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];
Strew answered 7/6, 2014 at 1:58 Comment(2)
Will this reload the entire page only for this?Oldline
This won´t work, as you are working on a copy of the WKWebView´s configuration (property has copy-flag). Only time where you can set a configuration is at WKWebView init.Chekhov
B
3

Here's something that's working for me:

Create an extension on WKWebView that defines a 'runJavaScriptInMainFrame:' method. In the extension method, use NSInvocationOperation to call the undocumented '_runJavaScriptInMainFrame:' method.

extension WKWebView {
    func runJavaScriptInMainFrame(#scriptString: NSString) -> Void {
        let selector : Selector = "_runJavaScriptInMainFrame:"
        let invocation = NSInvocationOperation(target: self, selector: selector, object: scriptString)
        NSOperationQueue.mainQueue().addOperation(invocation)
    }
}

To use, call:

webview.runJavacriptInMainFrame:(scriptString: "some javascript code")

Thanks to Larsaronen for providing the link to the private API for WKWebView.

Boost answered 13/6, 2014 at 14:26 Comment(2)
Wouldn't this be rejected in the app store approval process for using an undocumented/private API?Phifer
@Larsaronen, yes, I think you are correct: most likely this code would cause the app to be rejected. I was looking to test some JavaScript in a WKWebView, so used this as a temporary fix. Should have mentioned that this should not be used in production app. Thanks for the reminder!Boost
K
1

I just started digging around the WKWebView API myself so this might not be the best way, but I think you could do it with the following code:

NSString *scriptSource = @"console.log('Hi this is in JavaScript');";

WKUserScript *userScript = [[WKUserScript alloc]
    initWithSource:scriptSource
    injectionTime:WKUserScriptInjectionTimeAtDocumentStart 
    forMainFrameOnly:YES];

[myWKController addUserScript:userScript];

(from the WWDC'14 talk)

Kiarakibble answered 5/6, 2014 at 0:4 Comment(2)
Sorry – I didn't want to run the script at load time, but later on in in response to some action on the native side.Uncharted
This actually doesn't work; the addUserScript stuff only works before page load.Violetavioletta
W
1

Rumor has it that this is a bug because there's a private function similar(?) to what's publicly available in the UIWebView for evaluating javascript from obj-C.

Weikert answered 6/6, 2014 at 0:41 Comment(1)
Yes there is a private API for this: github.com/WebKit/webkit/blob/master/Source/WebKit2/UIProcess/… But why would they make it private.. Seems intentional.Phifer
H
1

Example of method, that runs JS code to load .pdf from base64String

  private func setData(_ base64String: String) {
    webView.evaluateJavaScript(
        "externalInterfaceLoadBase64('\(base64String)', scale = 1.0)",
        completionHandler: { _, error in
            if let error = error {
                debugPrint("Evaluating JavaScript error: \(error)")
            }
        }
    )
}
Handmedown answered 20/7, 2021 at 16:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.