UIWebView JavaScript losing reference to iOS JSContext namespace (object)
Solution 1
The page load can cause the WebView (and UIWebView which wraps WebView) to get a new JSContext.
If this was MacOS we were talking about, then as shown in the section on WebView in the 2013 WWDC introduction "Integrating JavaScript into Native Apps" session on Apple's developer network (https://developer.apple.com/videos/wwdc/2013/?id=615), you would need to implement a delegate for the frame load and initialise your JSContext variables in your implementation of the selector for webView:didCreateJavaScriptContext:forFrame:
In the case of IOS, you need to do this in webViewDidFinishLoad:
-(void)webViewDidFinishLoad:(UIWebView *)view{
self.js = [view valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // Undocumented access to UIWebView's JSContext
self.js[@"ios"] = self;
}
The previous JSContext is still available to Objective-C since you've kept a strong reference to it.
Solution 2
check this UIWebView JSContext
The key point is register a javascript object once JSContext changed. I use a runloop observer to check is there any network operation finished, once it finished, I'll get the changed JSContext, and register any object I want to it.
I didn't try if this work for iframe, if u have to register some objects in iframe, try this
NSArray *frames = [_web valueForKeyPath:@"documentView.webView.mainFrame.childFrames"];
[frames enumerateObjectsUsingBlock:^(id frame, NSUInteger idx, BOOL *stop) {
JSContext *context = [frame valueForKeyPath:@"javaScriptContext"];
context[@"Window"][@"prototype"][@"alert"] = ^(NSString *message) {
NSLog(@"%@", message);
};
}];
Related videos on Youtube
Comments
-
Madbreaks almost 2 years
I've been working on a proof of concept app that leverages two-way communication between Objective C (iOS 7) and JavaScript using the WebKit JavaScriptCore framework. I was finally able to get it working as expected, but have run into a situation where the UIWebView loses its reference to the iOS object that I've created via JSContext.
The app is a bit complex, here are the basics:
- I'm running a web server on the iOS device (CocoaHTTPServer)
- The UIWebView initially loads a remote URL, and is later redirected back to
localhost
as part of the app flow (think OAuth) - The HTML page that the app hosts (at localhost) has the JavaScript that should be talking to my iOS code
Here's the iOS side, my ViewController's
.h
:#import <UIKit/UIKit.h> #import <JavaScriptCore/JavaScriptCore.h> // These methods will be exposed to JS @protocol DemoJSExports <JSExport> -(void)jsLog:(NSString*)msg; @end @interface Demo : UIViewController <UserInfoJSExports, UIWebViewDelegate> @property (nonatomic, readwrite, strong) JSContext *js; @property (strong, nonatomic) IBOutlet UIWebView *webView; @end
And the pertinent parts of the ViewController's
.m
:-(void)viewDidLoad { [super viewDidLoad]; // Retrieve and initialize our JS context NSLog(@"Initializing JavaScript context"); self.js = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // Provide an object for JS to access our exported methods by self.js[@"ios"] = self; // Additional UIWebView setup done here... } // Allow JavaScript to log to the Xcode console -(void)jsLog(str) { NSLog(@"JavaScript: %@", str); }
Here is the (simplified for the sake of this question) HTML/JS side:
<html> <head> <title>Demo</title> <script type="text/javascript"> function setContent(c, noLog){ with(document){ open(); write('<p>' + c + '</p>'); close(); } // Write content to Xcode console noLog || ios.jsLog(c); } </script> </head> <body onload="javascript:setContent('ios is: ' + typeof ios)"> </body> </html>
Now, in almost all cases this works beautifully, I see
ios is: object
both in the UIWebView and in Xcode's console. Very cool. But in one particular scenario, 100% of the time, this fails after a certain number of redirects in the UIWebView, and once the above page finally loads it says:ios is: undefined
...and the rest of the JS logic quits because the subsequent call to
ios.jsLog
in thesetContent
function results in an undefined object exception.So finally my question: what could/can cause a JSContext to be lost? I dug through the "documentation" in the JavaScriptCore's .h files and found that the only way this is supposed to happen is if there are no more
strong
references to theJSContext
, but in my case I have one of my own, so that doesn't seem right.My only other hypothesis is that it has to do with the way in which I'm acquiring the
JSContext
reference:self.js = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
I'm aware that this may not be officially supported by Apple, although I did find at least one SO'er that said he had an Apple-approved app that used that very method.
EDIT
I should mention, I implemented
UIWebViewDelegate
to check the JSContext after each redirect in the UIWebView thusly:-(void)webViewDidFinishLoad:(UIWebView *)view{ // Write to Xcode console via our JSContent - is it still valid? [self.js evaluateScript:@"ios.jsLog('Can see JS from obj c');"]; }
This works in all cases, even when my web page finally loads and reports
ios is: undefined
the above method simultaneously writesCan see JS from obj c
to the Xcode console. This would seem to indicate the JSContext is still valid, and that for some reason it's simply no longer visible from JS.
Apologies for the very long-winded question, there is so little documentation on this out there that I figured the more info I could provide, the better.
-
Mahesh over 9 yearsHi Madbreaks, I have question related to communication between JS and IOS. If you have any idea to solve, please let me know. Below is my link "stackoverflow.com/questions/27120280/…"
-
Madbreaks about 10 yearsThis is exactly what I ended up doing, and it works great. I also do some checking in
webViewDidFinishLoad
to make sure the view's current URL is "my" URL so that I don't set up the context for every single page loaded. -
Madbreaks about 10 yearsWhat made sense to me was, with each new page load the UIWebView's
window
object is reset, and since that's what I'm attaching to when I doself.js[@'ios'] = self
, the reference was being lost. Is that accurate? Thanks Mike! -
Mike about 10 yearsYes. The window object is new on a page load, as is the DOM object tree. It isn't exactly clear to me what caching / reuse strategy is occurring for the JSContext on the page load (i would expect something to happen for performance reasons), but its clear that Apple expects you to plan on it getting replaced / a new one creating created on a page load. Things would be much simpler if Apple documented a formal way to get the JSContext for a UIWebView.
-
malhal almost 9 yearsNot sure about that linked code, would have been simpler just to use didFinishLoad and get the context there.
-
malhal almost 9 yearsWith this technique sometimes invoking the context gives an EXC_BAD_ACCESS anyone know how to fix that?
-
buaacss over 8 yearsYeah, that's would be simpler, but we noticed that for some case that doesn't work.
-
Martin over 8 yearsObserving that the registered JavaScript object disappears on each page reload on iOS 8.4. The marked solution in this thread did not solve my problem, but this answer fixed it. I guess Apple changed their APIs or something recently.
-
Adam Freeman over 8 yearsWhat is the equivalent of this code chunk in Swift? I tried something like js["ios"] = self but that does not work. I get a compile-time error.