• Stars
    star
    417
  • Rank 103,829 (Top 3 %)
  • Language
    Objective-C
  • Created about 11 years ago
  • Updated about 11 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A method for obtaining UIWebView's internal JSContext without using KVC

I've worked on more “hybrid" iOS applications than I care to admit. One of the major pain points of these apps is always communication across the web/native boundary - between JavaScript running in a UIWebView to ObjectiveC in the app shell.

We all know that the only official way to call into a UIWebView from ObjectiveC is via stringByEvaluatingJavaScriptFromString. And the typical way to call out from JavaScript is some manner of setting window.location to trigger a shouldStartLoadWithRequest: callback on the UIWebView delegate. Another oft-considered technique is to implement a custom NSURLProtocol and intercept requests made via XMLHttpRequest.

Apple gives us a public JavaScriptCore framework (part of WebKit) in iOS7, and JavaScriptCore provides simple mechanisms to proxy objects and methods between ObjectiveC and the JavaScript “context”. Unfortunately, while it is common knowledge that UIWebView is built upon WebKit and in turn is using JavaScriptCore, Apple didn’t expose a mechanism for us to access this infrastructure.

It is possible to manhandle UIWebView and retrieve its internal JSContext object by using a KVO keypath to access undocumented properties deep within UIWebView. impathic describes this technique on his blog. A major drawback of this approach, of course, is that it relies on the internal structure of the UIWebView.

I present an alternative approach to retrieving a UIWebView’s JSContext. Of course mine is also non-Apple-sanctioned, and could break also. I probably won’t try shipping this in an application to the App Store. But it is less likely to break, and I think too that it does not have any specific dependency on the internals of UIWebView other than UIWebView itself using WebKit and JavaScriptCore. (There is one small caveat to this, explained later.)

The basic mechanism in play is this: WebKit communicates “frame loading events” to its clients (such as UIWebView) using “WebFrameLoadDelegate” callbacks performed in similar fashion to how UIWebView communicates page loading events via its own UIWebViewDelegate. One of the WebFrameLoadDelegate methods is webView:didCreateJavaScriptContext:forFrame: Like all good event sources, the WebKit code checks to see if the delegate implements the callback method, and if so makes the call. Here’s what it looks like in the WebKit source (WebFrameLoaderClient.mm):

if (implementations->didCreateJavaScriptContextForFrameFunc) {
    CallFrameLoadDelegate(implementations->didCreateJavaScriptContextForFrameFunc, webView, @selector(webView:didCreateJavaScriptContext:forFrame:),
        script.javaScriptContext(), m_webFrame.get());
}

It turns out that in iOS, inside UIWebView, whatever object is implementing WebKit WebFrameLoadDelegate methods doesn’t actually implement webView:didCreateJavaScriptContext:forFrame:, and hence WebKit never performs this call. If the method existed on the delegate object then it would be called automatically.

Well, there are a handful of ways in ObjectiveC to dynamically add a method to an existing class or object. The easiest way is via a category. My approach hinges on extending NSObject via a category, to implement webView:didCreateJavaScriptContext:forFrame:.

Indeed, adding this method prompts WebKit to start calling it, since any object (including some sink object inside UIWebView) that inherits from NSObject now has an implementation of webView:didCreateJavaScriptContext:forFrame:. If the sink inside UIWebView were to implement this method in the future then this approach would likely silently fail since my implementation on NSObject would never be called.

When our method is called by WebKit it passes us a WebKit WebView (not a UIWebView!), a JavaScriptCore JSContext object, and WebKit WebFrame. Since we don’t have a public WebKit framework to provide headers for us, the WebView and WebFrame are effectively opaque to us. But the JSContext is what we’re after, and it’s fully available to us via the JavaScriptCore framework. (In practice, I do end up calling one method on the WebFrame, as an optimization.)

The question becomes how to equate a given JSContext back to a particular UIWebView. The first thing I tried was to use the WebView object we’re handed and walk up the view hierarchy to find the owning UIWebView. But it turns out this object is some kind of proxy for a UIView and is not actually a UIView. And because it is opaque to us I really didn’t want to use it.

My solution is to iterate all of the UIWebViews created in the app (see the code to see how I do this) and use stringByEvaluatingJavaScriptFromString: to place a token “cookie” inside its JavaScriptContext. Then, I check for the existence of this token in the JSContext I’ve been handed - if it exists then this is the UIWebView I’ve Been Looking For!

Once we have the JSContext we can do all sorts of nifty things. My test app shows how we can map ObjectiveC blocks and objects directly into the global namespace and access/invoke them from JavaScript.

One Last Thing

(Sorry, couldn’t resist.) With this bridge in place, it’s now possible to invoke ObjectiveC code in the app via the desktop Safari WebInspector console! Try it out with the test app by attaching the WebInspector and typing sayHello(); into the console input field. Now that’s cool!

Nick Hodapp (a.k.a TomSwift), November 2013