Measure Once, Cut Twice

Notes on using MacRuby and WebView

Posted in macruby, objective-c by steve on October 29, 2010

After much head bashing, the following works for two-way calling between MacRuby and Javascript within a WebKit WebView. Key issues:

  • Get the script object via the callback method. Many tutorials show obtaining it via [webView windowScriptObject]. This does not always work since the script object may not be ready (e.g. the page isn’t fully loaded).
  • Take note of which delegate methods are static (isSelectorExcludedFromWebScript) and which are not.
  • MacRuby names that you want callable from javascript must consist only of lower case characters. The mappings given in the docs (fooBar: converted to fooBar_, foo_bar converted to foo$_bar) have some idiosyncrasies.
    • For no-args methods, everything works smoothly as long as you use all lowercase ruby method names with no underscores.
    • If you want to pass an argument, you need to call it from JS with an underscore, declare it in macruby without the underscore, and also register it via ‘respondsToSelector’. To summarize:
      • Define method in macruby: mymethod(somearg)
      • Call from javascript: webscriptObj.mymethod_(somearg)
      • In the webView initialization on the object that contains mymethod(somearg): self.respondsToSelector(‘mymethod:’)
      • All of this confusion seems to arise from translation between JS methods, selectors, and strings/symbols in macruby. The colon at the end matters and it doesn’t work if it is a symbol, :’mymethod:’. For no-args methods though symbols work just fine.
  • It is handy setting up the delegate methods to trap the console.(log|error|warn) methods as well as window.status changes.
  • Note the WebScriptObject does not exist for the iPhone UIWebView, only for the OSX WebKit WebView. The PhoneGap project has a workaround for UIWebView for call from Javascript to Objective-C. For the opposite direction, Objective-C calling Javascript, the method stringByEvaluatingJavaScriptFromString is portable across both UIKit and WebKit.
class MyMacRubyController

    attr_accessor :scriptobj

    # In this example, the top level app_controller creates MyMacRubyController
    # and initializes it with a WebView created via Interface Builder.
    def initialize(view)
        @view = view
        #@view.delegate = self   # only for UIWebView, below are for WebView
	@view.UIDelegate = self
	@view.frameLoadDelegate = self
	@view.setMainFrameURL(NSBundle.mainBundle.pathForResource(‘test.html’, ofType:nil))
        set_body_content()
    end

    def set_body_content(name)
        js = ‘document.getElementById(“content”).innerHTML = “some html text”’
        @view.stringByEvaluatingJavaScriptFromString(js)
    end

    #### Methods exposed to javascript via ‘myController’ object ####

    def test
        puts “In test method”
    end

    def test(somearg)
        puts "In test method with arg: #{somearg}"
    end

    #### WebScripting delegate related ####

    # This WebScripting delegate method required and must be static.
    # Ensures only methods you want are exposed to javascript.
    def self.isSelectorExcludedFromWebScript(selector)
        if selector == :test or selector == 'test:'
            return false
        else
            return true
        end
    end

    # Delegate method for obtaining the script object. Sets up the main
    # application callback object, myController. MacRuby method names
    # seem to need to be all lowercase characters e.g. ‘runme’, ‘test’
    def webView(wv, windowScriptObjectAvailable:obj)
        @scriptobj = obj
        puts “Got window script object #{@scriptobj}”
        @scriptobj.setValue(self, forKey:’myController’)
        self.respondsToSelector('test:')    # this registration is only needed for methods with args
    end

    # Called whenever window.status is called in JS
    def webView(wv, setStatusText:text)
        puts “JS status -> “ + text
    end

    # Private WebViewUIDelegate method to trap console.log messages
    def webView(wv, addMessageToConsole:message)
        puts “JS console -> “ + message[‘message’]
    end
end
<html>
<head>
<script type=“text/javascript”>
function doSomething() {
    console.log(“in dosomething”)
    window.status = “status set in dosomething”
    console.log(window.myController);
    console.log(window.myController.test());
    console.log(window.myController.test_("arg passed"));
}
</script>
</head>

<body>
<form>
  <button type=“button” onclick=“doSomething()”>Click me</button>
</form>
<div id=“content”></div>
</body>
</html>

Update: found this related article from the merbist.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: