Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WKWebView.loadPlugin() does not add plugin to 'window' in the webView's JS environment. #72

Closed
shirakaba opened this issue Feb 16, 2017 · 7 comments

Comments

@shirakaba
Copy link

shirakaba commented Feb 16, 2017

I am trying to call a Swift function (which returns a String array) from the JavaScript environment of my webView.

I have copied your quick start instructions and tried your sample app. I am receiving no compile-time errors, yet when I run webView.evaluateJavascript(), no reference to my plugins are ever found.

Here is my ViewController.swift:

// Copyright shirakaba 2017; https://github.com/shirakaba
import WebKit
import UIKit
import JavaScriptCore
import XWebView
import Foundation

class Tokenising: NSObject {
    static func romaniseobj(input: String) -> [String] {
        print("Got asked to romanise \(input)")
        return ["example", "return", "value"]
    }
}

class HelloWorld : NSObject {
    func show(text: AnyObject?) {
        let title = text as? String
        DispatchQueue.main.async {
            let alert = UIAlertView(title: title, message: nil, delegate: nil, cancelButtonTitle: "OK")
            alert.show()
        }
    }
}


class ViewController: UIViewController, WKNavigationDelegate, UISearchBarDelegate, WKScriptMessageHandler {
    var searchBar: UISearchBar = UISearchBar()
    var progressView: UIProgressView = UIProgressView(progressViewStyle: .bar)
    var webView: WKWebView!
    let jsvm: JSVirtualMachine = JSVirtualMachine()
    let injectlib = try! String(contentsOfFile: Bundle.main.path(forResource: "inject3", ofType: "js")!)
    
    override func loadView() {
        super.loadView()
        
        let contentController = WKUserContentController();
        let userScript = WKUserScript(
            source: "",
            injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
            forMainFrameOnly: true
        )
        contentController.addUserScript(userScript)
        contentController.add(self, name: "callbackHandler")
        
        let config = WKWebViewConfiguration()
        config.userContentController = contentController
        
        self.webView = WKWebView(frame: self.view.frame, configuration: config)
        webView.loadPlugin(HelloWorld(), namespace: "HelloWorld")
        webView.loadPlugin(Tokenising(), namespace: "Tokenising")
        self.view = self.webView!
    }
    
    func userContentController(_ userContentController: WKUserContentController,didReceive message: WKScriptMessage) {
        if(message.name == "callbackHandler") {
            print("JavaScript is sending a message \(message.body)")
            // print(romanise(text: message.body as! String))
        }
    }

    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /** Set up webView. */
        webView.frame = self.view.frame // Brought over from UIWebView, but probably not necessary.
        webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
        
        /** Construct toolbar */
        let progressButton = UIBarButtonItem(customView: progressView)
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(webView.reload))
        toolbarItems = [progressButton, spacer, refresh]
        
        /** Construct searchBar */
        searchBar.delegate = self // required to link this to searchBarSearchButtonClicked() which is a delegated function
        searchBar.searchBarStyle = UISearchBarStyle.minimal
        searchBar.showsCancelButton = true
        
        /** Set up navigationItem and its controller */
        navigationItem.titleView = searchBar
        navigationController?.isToolbarHidden = false
        navigationController?.hidesBarsOnSwipe = true
        
        let url = URL(string: "http://www.motherfuckingwebsite.com")!
        webView.load(URLRequest(url: url))
        webView.allowsBackForwardNavigationGestures = true
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
    }
    
    /** On webView navigation complete. */
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        webView.evaluateJavaScript("window.helloWorld.show();", completionHandler: { result, error in
            print("Handled helloWorld.show() to completion.")
            print(result ?? "no result from optional 'result'.")
            print(error ?? "no result from optional 'error'.")
        })

        webView.evaluateJavaScript("window.Tokenising.romaniseobj('好好干!');", completionHandler: { result, error in
            print("Handled Tokenising.romaniseobj() to completion.")
            print(result ?? "no result from optional 'result'.")
            print(error ?? "no result from optional 'error'.")
        })
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "estimatedProgress" {
            progressView.progress = Float(webView.estimatedProgress)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

Upon webView finishing navigation, I'd like to call my native function Tokenising.romaniseobj(). However, despite loading both classes in as plugins, neither 'Tokenising' nor 'HelloWorld' appear on the webView's JS environment's window. When webView.evaluateJavascript("window.HelloWorld.show();") is called, I receive the following error message:

Error Domain=WKErrorDomain
Code=4 "A JavaScript exception occurred"
UserInfo=
    {
        WKJavaScriptExceptionLineNumber=1,
        WKJavaScriptExceptionMessage=TypeError: window.HelloWorld.show is not a function. (In 'window.HelloWorld.show()', 'window.HelloWorld.show' is undefined' is undefined),
        WKJavaScriptExceptionSourceURL=http://www.motherfuckingwebsite.com/,
        NSLocalizedDescription=A JavaScript exception occurred,
        WKJavaScriptExceptionColumnNumber=30
    }

When I print out every property on 'window', I find that indeed, there is no 'HelloWorld' property. This same error message occurs for calling Tokenising.romaniseobj(). What am I doing wrong?

@xwv
Copy link
Contributor

xwv commented Feb 16, 2017

  1. Only instance methods will be exposed. In your case, remove static keyword or loadPlugin(Tokenising.self, namespace: "Tokenising")
  2. Return value of method can not be passed to javascript, see Return from methods #52 .

@xwv
Copy link
Contributor

xwv commented Feb 16, 2017

webView.evaluateJavaScript("window.helloWorld.show();", completionHandler: {

It should be window.HelloWorld.show(), because you registered the object in "HelloWorld" namespace. it is case sensitive.

@shirakaba
Copy link
Author

Thank you for your quick response!

I've tried several things, on your advice:

  1. Loading the plugin (as written above), but as Tokenising.self.

  2. Removing the static keyword on the romaniseobj function and loading the plugin Tokenising().

  3. Fixing the letter case on 'HelloWorld' (sorry, it was correct in the source code, but I mistyped it when copying it to this Issue).

... but the error result is still exactly the same as written above.

@shirakaba
Copy link
Author

Return value of method can not be passed to javascript, see #52 .

I see. I'll try rewriting my Tokenising.romaniseobj function tonight when I have time.

@xwv
Copy link
Contributor

xwv commented Feb 16, 2017

You can use safari attach the webview to check whether the user scripts generated by XWebView is injected into page. Please paste here if you find them.

I can't find the root cause by eyes. You need to minimize the code of the test case to demonstrate the problem. If you can share a buildable project, I will try to debug it.

@shirakaba
Copy link
Author

shirakaba commented Feb 17, 2017

Just finished some further investigation. Looks like I was wrong about there being no 'Tokenising' or 'HelloWorld' object on the window (I simply didn't see it - my bad), but the function names were not what I'd expected, and that was causing the error.

func show(text: AnyObject?) {
    /* ... */
}

The problem is that no external name had been set for the 'text' parameter of the show function. Thus, the Javascript function name became window.HelloWorld.showWithText, which was quite bewildering. Equally, it produced window.Tokenising.romaniseobjWithInput.

This is easily fixed by making the external parameter name anonymous (just adding an underscore before the parameter's internal name):

func show(_ text: AnyObject?) {
    /* ... */
}

This way, the function is accessible in JS, as expected, as window.HelloWorld.show.

Perhaps the naming convention for functions has changed since the 2015 Quick Start guide? Or has this always been the case, but Swift's behaviour has changed..? In any case, it may be worth updating this part of the Quick Start guide.

Thank you very much for your quick help. I'll investigate how to return a value from the method by referring to #52 . If I have any problems with that part, I'll add a reply under that issue page. This current issue can be closed.

@xwv
Copy link
Contributor

xwv commented Feb 21, 2017

The guide was quite old. The problem caused by the swift change SE-0046.
Thanks for updating the guide.

BTW, I keep updating the sample app with the library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants