UIWebView JavaScript losing reference to iOS JSContext namespace (object)
Asked Answered
T

2

18

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 the setContent 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 the JSContext, 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 writes Can 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.

Tropology answered 11/2, 2014 at 22:21 Comment(1)
Hi 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 "#27120780"Infantryman
M
17

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.

Mattson answered 12/2, 2014 at 5:27 Comment(5)
This 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.Tropology
What 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 do self.js[@'ios'] = self, the reference was being lost. Is that accurate? Thanks Mike!Tropology
Yes. 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.Mattson
With this technique sometimes invoking the context gives an EXC_BAD_ACCESS anyone know how to fix that?Cervin
What 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.Anhwei
D
4

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);
    };
}];    
Disgusting answered 27/5, 2015 at 15:51 Comment(3)
Not sure about that linked code, would have been simpler just to use didFinishLoad and get the context there.Cervin
Yeah, that's would be simpler, but we noticed that for some case that doesn't work.Disgusting
Observing 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.Baronet

© 2022 - 2024 — McMap. All rights reserved.