Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confirmation of Javascript behavior in a UIWebView

I am trying to execute some Javascript on content within a UIWebView. Perhaps my knowledge of the Javascript runtime is lacking, but I"m confused about the following example. See sourcecode and comments for details:

NSString *html = [NSString stringWithFormat:@"<html><head><script type='text/javascript'>var content='the initial content';function myFunc(){return 'value of content: ' + content;}myFunc();</script></head><body>Hello blank</body></html>"];
// I would expect after this call that the variable content exists, as well as the function myFunc()
[self.webView loadHTMLString:html baseURL:nil];
// However, it appears not to.  The following call returns nothing
NSString *result = [self.webView stringByEvaluatingJavaScriptFromString:@"myFunc();"];
NSLog(@"result: %@", result);

// Inserting the Javascript at this point seems to work as expected
result = [self.webView stringByEvaluatingJavaScriptFromString:@"var content='the new content';function myFunc(){return 'value of content: ' + content;}myFunc();"];
NSLog(@"result: %@", result);

// And calling myFunc() at this point is successful
result = [self.webView stringByEvaluatingJavaScriptFromString:@"myFunc();"];
NSLog(@"result: %@", result);

To sum up, I would expect that the Javascript global variable and function I created when i load the html to be available to later javascript calls. Surprisingly, it appears not to, and all javascript stuff must be added later. Is this correct?

thanks for any help in advance.

----- update ----- I should add that self.view is a WebView in this case. Also, I have tried putting the script tag in both the head and body of the HTML doc, with no change in behavior.

like image 796
D.C. Avatar asked Jan 18 '11 05:01

D.C.


1 Answers

mclin's answer is correct in that UIWebView performs the loads in the background. If all you're loading is plain html and no tags have src="..." type content then you can be reasonably certain that webViewDidFinishLoad (UIWebViewDelegate protocol method) will be called only once and that you can perform your javascript in that delegate method.

However, UIWebView will, after loading the initial HTML start loading any images, javascript, audio, video, etc and webViewDidFinishLoad will be called as each of those subsequent loads complete. I think if you watch the progress bar on Safari on your iPhone, you can kind of see this behavior. The progress bar goes about half-way after the initial page loads and then marches on a little at a time as the images or what have you are loaded.

I've tried a couple of techniques to run javascript after the page load is complete. The easier is to have webViewDidFinishLoad perform another selector after a brief delay, and check the loading property of the webView. You are taking a bit of a risk here in that we bring "timing" and "threads" together, but hey ... if you want easy (ier that is).

-(void) checkIfLoadDone:(UIWebView *) webView {
  if (webView.loading) { return; }
  // run your javascript here
}

-(void) webViewDidFinishLoad:(UIWebView *) webView {
  [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                           selector:@selector(checkIfLoadDone:)
                                             object:webView];
  [self performSelector:@selector(checkIfLoadDone:) 
             withObject:webView 
             afterDelay:0.5];
}

-(void) webViewDidStartLoad:(UIWebView *)webView {
  [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                           selector:@selector(checkIfLoadDone:)
                                             object:webView];
}

- (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
  // ooops ... probably should deal with the error, but lets just run checkIfLoadDone
  [NSObject cancelPreviousPerformRequestsWithTarget:self 
                                           selector:@selector(checkIfLoadDone:)
                                             object:webView];
  [self performSelector:@selector(checkIfLoadDone:) 
             withObject:webView 
             afterDelay:0.5];
}

A more complex technique is to track the loads individually and maintain a counter. When the counter returns to 0, you can run your javascript. In that case you'll want to implement the delegate method for

- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  if (navigationType != UIWebViewNavigationTypeOther) {
    self.outStandingLoads = 0;
  }
  return YES;
}

Now you can have webViewDidStartLoad: increment outStandingLoads and have webViewDidFinishLoad: and webView:didFailLoadWithError: decrement and check outStandingLoads. I'd use an atomic NSInteger property for outStandingLoads throughout the code to address the potential for there being more than one thread doing the loading.

I think either method will work reasonably well. I chose the latter approach, but only because I needed to address other issues in the webViewDelegate calls. I decided to extend UIWebView and make it its own delegate where my extension implemented the delegate methods. I then leverage the NSNotification model to post notifications like "WebPageLoadBegan" and "WebPageLoadComplete" to communicate with my user interface code. It seemed like an easier model to me when working with two different sets of UI code ... the iPad and iPhone.

I hope this helps.

like image 159
Drew Gonczi Avatar answered Oct 04 '22 01:10

Drew Gonczi