October 18, 2010

Using Multiple UIWebView Objects

Posted in Code Snippets, iOS at 12:41 pm by Ben

If you’ve ever tried to put multiple UIWebView objects on screen and load documents into all of them at once, you may have stumbled across an undocumented limitation of the iPhone SDK: the loadRequest method on UIWebView is not re-entrant, and you’re often left with one or more of the views showing an error icon.  Fortunately, it’s very easy to leverage the UIWebViewDelegate protocol to resolve this issue, by ensuring that only one of the views is loading at a time.

Create a class that implements the UIWebViewDelegate protocol and includes a class-level UIWebView* variable named spinlock, then implement the following methods:

-(void)spinlockedWebViewLoad:(NSArray*)arr {
  NSAutoreleasePool* pool=[[NSAutoreleasePool alloc] init];
  UIWebView* webView=(UIWebView*)[arr objectAtIndex:0];
  NSURLRequest* request=(NSURLRequest*)[arr objectAtIndex:1];
  @synchronized(self) {
    spinlock=webView;
    [webView loadRequest:request];
    while (spinlock==webView) [NSThread sleepForTimeInterval:0.1];
  }
  [pool drain];
}
-(BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
  if (spinlock!=webView) {
    [NSThread detachNewThreadSelector:@selector(spinlockedWebViewLoad:) toTarget:self withObject:[NSArray arrayWithObjects:webView,request,nil]];
    return NO;
  }
  return YES;
}
-(void)webViewDidFinishLoad:(UIWebView*)webView {
  spinlock=nil;
}
-(void)webview:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
  spinlock=nil;
}

Set your multiple web views to use the same instance of this object as their delegate before loading.  (It’s often easiest to make the object that calls them be this delegate.)  Here’s how it works:

Our own method spinlockedWebViewLoad will run on an alternate thread for each attempted load request coming from any UIWebView with this as its delegate.  We use the class-level variable spinlock to track which UIWebView is currently allowed to perform a load operation (this is used in the next method).  By synchronizing, we allow only one thread at a time to hold this spinlock, and it remains in the while loop inside the synchronized block until some other thread unsets the spinlock.

Any attempt to load, whether from your code or from the user clicking a link on the page already in the view, will ask your code whether it should load by calling the delegate method shouldStartLoadWithRequest.  In our implementation of that method, we check whether the UIWebView that wants to load is the one (the only one) currently “allowed” to load, by checking whether it’s the one that spinlock is currently set to.  If not, we tell it no, it’s not allowed to proceed with the load request, but we kick off a new thread on the above method, essentially requeueing that same request.  Of course, if the UIWebView is the one that has the spinlock, it returns yes so that the load may proceed.

When the load either finishes or fails, we release the spinlock.  The thread which is currently blocked by the while loop unblocks, exits the synchronized block, and one of the other threads waiting to enter the synchronized block (if any) is allowed to do so.

Basically, spinlockedWebViewLoad is traffic control, the delegate method shouldStartLoadWithRequest ensures that all requests do go through traffic control (as long as the UIWebView has its delegate set), and webViewDidFinishLoad and didFailLoadWithError signal traffic control that one load is done and the next can begin.

The only functionality that you may lose (and you can add more code to effectively regain it) is if you need to use these delegate methods for other functionality as well.  It’s generally still doable by adding the code to these methods, but if you need to operate on the navigationType parameter of shouldStartLoadWithRequest, it can be tricky because by the time we allow the load to proceed, the type is always “other” – navigation kicked off by our code, not a user click.  If you absolutely need this info when loading, I recommend trying to record the navigation type in an NSDictionary (indexed by the UIWebView in which it occurs) when the load is blocked, on the assumption that that’s the “real” navigation, and pull it back out when loading is allowed.  But this occurs far less often than the larger issue of multiple web views, so I’ll leave the actual code as an exercise to the reader.

About these ads

4 Comments »

  1. Piotr said,

    -(BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:
    (…)
    if (spinlock!=webView) {
    [NSThread deatchNewThreadSelector:@selector(spinlockedWebViewLoad:) toTarget:self withObject:[NSArray arrayWithObjects:webView,request,nil]];
    (…)

    It should be [NSThread detachNewThreadSelector … not “deatch”.

    • Ben said,

      Thanks for the catch! I’ve corrected it in the post.

  2. [...] found this code block here for UIWebiViewDelegate but I could not successfully convert it to [...]

  3. harsh said,

    Thanks for posting. but how can i use it. simple project code display.

    Thanks


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: