web-dev-qa-db-fra.com

Appelez la fonction JavaScript à partir du code natif dans WKWebView

À l'aide d'une WKWebView dans iOS 8, comment puis-je exécuter une fonction JavaScript du côté natif ou communiquer du JavaScript du côté natif? Il ne semble pas exister de méthode apparentée à stringByEvaluatingJavaScriptFromString: De UIWebView.

(Je peux utiliser - addScriptMessageHandler:name: Sur l'objet configuration.userContentController Pour permettre la communication de JS vers natif, mais je cherche la direction opposée.)

42
Sophie Alpert

(J'ai déposé un radar pour cela peu après avoir posé la question ici.)

Une nouvelle méthode a été ajoutée il y a quelques jours (merci jcesarmobile pour l'avoir signalée):

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

La méthode est disponible dans iOS 8 bêta 3 et plus. Voici la nouvelle signature de méthode:

/* @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; 

Les documents sont disponibles ici: https://developer.Apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript .

57
Sophie Alpert

Détails

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

La description

Le script est inséré dans une page qui sera affichée dans WKWebView. Ce script renverra l'URL de la page (mais vous pouvez écrire un autre code JavaScript). Cela signifie que l'événement de script est généré sur la page Web, mais qu'il sera traité dans notre fonction:

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)

Événements de capture

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

Exemple de code complet

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

ajoutez dans votre paramètre de sécurité de transport Info.plist

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

Résultat

enter image description here

Ressources

Propriétés et méthodes des objets de document

20
Vasily Bodnarchuk

Ce n'est peut-être pas une méthode idéale, mais en fonction de votre cas d'utilisation, vous pouvez simplement recharger WKWebView après avoir infecté le script utilisateur:

NSString *scriptSource = @"alert('WKWebView JS Call!')";

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

[wkWebView.configuration.userContentController addUserScript:userScript];
[wkWebView reload];
5
Brian Singer

Voici quelque chose qui fonctionne pour moi:

Créez une extension sur WKWebView qui définit une méthode 'runJavaScriptInMainFrame:'. Dans la méthode d'extension, utilisez NSInvocationOperation pour appeler la méthode non documentée '_runJavaScriptInMainFrame:'.

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

Pour l'utiliser, appelez:

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

Merci à Larsaronen d’avoir fourni le lien vers l’API privée de WKWebView.

2
GazingAtNavel

Selon la rumeur, il s'agirait d'un bogue, car il existe une fonction privée similaire à celle disponible publiquement dans UIWebView pour évaluer le javascript de obj-C.

0
iphone007

Je viens de commencer à explorer l'API WKWebView moi-même, alors ce n'est peut-être pas la meilleure solution, mais je pense que vous pourriez le faire avec le code suivant:

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

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

[myWKController addUserScript:userScript];

(extrait de la conférence WWDC'14)

0
Darshan Shankar