UIWebView-TS_JavaScriptContext

by TomSwift

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

414 Stars 79 Forks Last release: Not found 1 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

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

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.