Basic Multiple Window Support for iOS UIWebView

Basic Multiwindow UIWebView iOS

One of my recent projects required the use of a UIWebView inside of an iOS application so a web page could be loaded and viewed from within the app, rather than switching over to Safari to open the web page (you know, like apps such as Facebook and Twitter do). While this was not a difficult task, I ran into issues with web pages that used JavaScript to open windows, or pages that had anchor tags with a target of “_blank”. By default, it seems that in both of these instances, the target URL is simply loaded into the same web view, rather than being handled with multiple window support. I guess this makes sense to an extent, but it was unfortunately a roadblock I needed to break through for my application. So I investigated handling multiple window support, and after a few days of testing and debugging, I came up with a basic solution, which I’ll outline below.

Before reading on (or if you just want the solution), it’ll be most helpful to you if you grab the demo project I created on GitHub. I’ll briefly outline some highlights from the code in my explanation of the solution below, but you’ll want to actually look at the full solution to get the best feel for how everything is wired up. Plus, you can use the demo project code in your own application, if you so desire.

Fork Project on GitHub

Now, let’s get to the solution…

Step 1: Overriding JavaScript Window Methods

The first step in the solution is to write some custom JavaScript that will be injected into each UIWebView. In my case, I created overrides for the window.open and window.close methods. Why? So I could catch when one of these methods is called inside of Xcode and handle it appropriately. Ditto for overriding click handlers of anchor tags with a target of “_blank”.

The custom JavaScript that I wrote to inject into each of my web views looks like this:

(function () {
  'use strict';
  /*global document, window, setInterval, clearInterval */
  window.open = function (url, name, specs, replace) {
    var iframe = document.createElement('IFRAME');
    iframe.setAttribute('src', 'hdwebview://jswindowopenoverride||' + url);
    iframe.setAttribute('frameborder', '0');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    document.body.appendChild(iframe);
    document.body.removeChild(iframe);
    iframe = null;
    return;
  };
  window.close = function () {
    var iframe = document.createElement('IFRAME');
    iframe.setAttribute('src', 'hdwebview://jswindowcloseoverride');
    iframe.setAttribute('frameborder', '0');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    document.body.appendChild(iframe);
    document.body.removeChild(iframe);
    iframe = null;
  };
  window.hdMakeHandler = function (anchor) {
    return function () {
      window.open(anchor.getAttribute('href'));
    };
  };
  window.hdWebViewReadyInterval = setInterval(function () {
    if (document.readyState === 'complete') {
      var i, ab = document.getElementsByTagName('a'), abLength = ab.length;
      for (i = 0; i < abLength; i += 1) {         if (ab[i].getAttribute('target') === '_blank') {             ab[i].removeAttribute('target');             ab[i].onclick = window.hdMakeHandler(ab[i]);         }       }       clearInterval(window.hdWebViewReadyInterval);     }   }, 10); }());

 

A few quick notes about this JavaScript code for anyone who is curious:

  • In each of the override methods (window.open and window.close), a 1x1 iframe is created and added to the document body, then removed right away and set to null. Its src is set to a custom, funky looking URL that includes a custom URL scheme and "method" we need to use. It is set up this way, because in Xcode, I ended up sniffing for this custom URL scheme in the one of the web view delegate methods, and when I catch this URL scheme, handle it appropriately (more on that later).
  • When the document is ready, all anchor tags are gathered into an array, which is looped over to check for any with a target of "_blank". For all with a target of "_blank", the target attribute is removed and the anchor tag gets an onclick listener that fires off a window.open call (which we have already overridden to do our bidding).
  • The code above is formatted nicely. Inside of the Xcode source, however, all line breaks are removed and the JavaScript is one long line since it's set to be the value of a constant (kJSOverrides, in the demo project).
  • All of the JavaScript passes lint tests at JSLint.com. (You're welcome.)

(Note: If you need to do anything similar, or wish to build out a more robust browser, you can toy around with overriding more methods as needed. I just did these basic ones to handle open and close events, as well as overriding default anchor tag behavior for "_blank" targets.)

Step 2: Create UIWebView Instances Dynamically

Rather than baking one UIWebView into a xib in Xcode, I found it most helpful to create a convenience method to return me an instance of UIWebView, and add it to my view on the fly. Why? For multiple window support, I needed to have multiple web views, and needed to store references to them in an array (so I knew how many there were, which was the top-most one, etc). Doing it dynamically proved to be the best solution.

In my viewDidLoad method, I created an initial web view with this convenience method, and added it to my view. And as you'll see, I also use this method to create web views on the fly when a window.open JavaScript method is detected.

Below is the code I used to create a new web view.

- (UIWebView *) newWebView
{
  // Create a web view that fills the entire window, minus the toolbar height
  UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, (float)self.view.bounds.size.width, (float)self.view.bounds.size.height - 44)];
  webView.scalesPageToFit = YES;
  webView.delegate = self;
  webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  // Add to windows array and make active window
  [self.windows addObject:webView];
  self.activeWindow = webView;
  return webView;
}

 

A few things to note about this code:

  • The web view is created to take up the entire screen, minus the height of the toolbar (44px). Toolbar? Yeah, more on that in a minute.
  • The new web view is added to self.windows, an array of UIWebView elements. Also, the new web view is set to the self.activeWindow property, so I know which web view is my active one.

Step 3: Create a Toolbar

In order for my solution to work, I needed a way to be able to navigate back/forward in the web view - not only for ease of user experience, but because I ended up using the "back" button to close web views that weren't the original base web view. So for instance, if a user clicked something that called window.open method (which in turn I set up to create a new web view), they could click the "back" button on the toolbar and return to the base window - all without even noticing that they had been served up a second web view.

The toolbar was one thing that I actually did create in my xib file in Xcode, though it could be created dynamically if desired. I added a "back", "forward", and "refresh" button, and wired them up with methods I created in the implementation file to handle the events appropriately.

(Note: You can view the source code to see all of the logic for these buttons, as I won’t explain it here.)

Step 4: Inject the Custom JavaScript

Now that the web view and toolbar logic had been wired up, I needed to actually inject the custom JavaScript into the web view so it would do my bidding. This was super easy. In the web view webViewDidFinishLoad delegate method, the following line was added (where kJSOverrides is a constant created that holds our custom JavaScript code):

__unused NSString *jsOverrides = [webView stringByEvaluatingJavaScriptFromString:kJSOverrides];

By injecting this custom JavaScript into the web view when the page is done loading, we override the default window.open and window.close methods, as well as the default behavior for anchor tags with target of "_blank". And this works on any page loaded in to the web view, since it's injected each time a page is done loading.

Step 5: Hijack URL Requests

With all of the pieces in place and the JavaScript injected, I turned my attention to the web view webView:shouldStartLoadWithRequest:navigationType delegate method. In short, the logic in this method sniffs the request URL, and if it uses the specific prefix that was set up in the JavaScript window method override (hdwebview://), the method returns "NO" (meaning it should NOT start loading the request). Why shouldn't it start loading the request? Remember that bunk iframe we created and destroyed in our custom JavaScript? Yeah, we didn't use a legit URL, but did that to set a flag that we use to catch when one of the JavaScript methods have been called.

That said, you'll notice that there's conditional logic used when this URL scheme is caught, checking for what type of "method" has been detected. This might seem a bit hacky, but it works. Essentially, the URL scheme gets stripped off, and the rest of the URL is broken into an array where double pipes ("||") are found. The double pipes are used for the window.open method, so we can not only get the JavaScript method reference, but also the URL that is intended to be opened (found to the right of the double pipes).

If a window.open is detected, a new web view is created and loaded with the specified URL. If a window.close is detected, and there's more than one web view in our windows array, the web view is closed (via a method that destroys the top web view).

Here's a look at the webView:shouldStartLoadWithRequest:navigationType method:

- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
  // Check URL for special prefix, which marks where we overrode JS. If caught, return NO.
  if ([[[request URL] absoluteString] hasPrefix:@"hdwebview"]) {
    NSString *suffix = [[[request URL] absoluteString] stringByReplacingOccurrencesOfString:@"hdwebview://" withString:@""];
    NSArray *methodAsArray = [suffix componentsSeparatedByString:[HDString encodedString:@"||"]];
    NSString *method = [methodAsArray objectAtIndex:0];
    if ([method isEqualToString:@"jswindowopenoverride"]) {
      UIWebView *webView = [self newWebView];
      NSURL *url = [NSURL URLWithString:[NSString stringWithString:[methodAsArray objectAtIndex:1]]];
      NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
      [webView loadRequest:requestObj];
      [self.view addSubview:webView];
      [self refreshToolbar];
    } else if ([method isEqualToString:@"jswindowcloseoverride"]) {
      if ([self.windows count] > 1) {
        [self closeActiveWebView];
      }
    }
    return NO;
  }
  if (![webView isEqual:self.activeWindow]) {
    return NO;
  }
  return YES;
}

Wrapping Up

While this is by no means a robust solution, it does the basic job of handling the most common JavaScript window methods and anchor tags with target "_blank". You no doubt noticed that, unlike Safari and other full-fledged iOS browsers, there's no logic in place allowing the user to see how many windows are open, to manually navigate between them, etc. This was intentional for my particular solution, as I needed to be able to handle open/close methods and keep the user's place in all web views, all while appearing seamless to the end user (i.e. looking like it's in one window).

But wait - doesn't the "back" button do the job of taking the user back to their place on the previous page? Well, yes, in most cases. But on certain websites that rely on AJAX to change/refresh/update a page based on user interaction, or that have their own flow rules based on user actions (that don't rely on different HTML pages), navigating back to the last page could result in the user's info on that page being reset and force them to re-do what they had already done earlier.

You also likely noticed that this solution passes in the URL to load to the first web view, but provides no way for the user to input a URL or search (like Safari and other iOS browsers do). This too, was intentional for my particular solution, as we didn't want to give the user full control of the web view like it was a full, robust browser.

One thing I was not able to figure out yet is successfully handling window-to-window JavaScript communication - such as having a popup window call a method in the parent window. If anyone has a solution for that, I'd love to hear it in order to be able to figure it out and get it incorporated into this solution.

I hope you find this helpful for handling basic multiple window support in iOS. And again, if you figure out how to do window-to-window JavaScript communication, I'd love to hear about it.

Until next time, happy coding!

Update: June 10, 2014

Due to the question and feedback about injecting JavaScript methods into iframes on a page loaded into a web view, I did some investigating and testing, eventually updating the original JS override code (the original is still listed above in Step 1). In short, the updated JS override code now loops through all of the frames in the page loaded, and attempts to override the appropriate methods.

Note: The JS override methods only work on iframes of the same origin, due to cross-domain security rules. As such, the updated code uses a try/catch around the iframe window overrides, failing silently if it cannot override.

The updated override code is available in the source code of my 'iOS-Web-View-Multiwindow' Git Repo.

22 Responses to this post:

  • sergio says:

    thanks man, good solution! you give me some ideas.

  • Eric says:

    Good article
    I have met a trouble. I have a UIWebView, It can load a webpage first time,when i click on it ,i want open a new webView to load, what can i do?

  • Eric,

    I’m assuming that when you say “when i click on it, i want open a new webView”, you mean that you want a link inside the webView to launch a new webView. If that’s the case, make sure you are injecting the JavaScript overrides into your web view (as specified in the article). Also, make sure that the link(s) in the webpage you’re loading are either using “window.open()” or have an href=”_blank” attribute on the anchor tag.

    The project on GitHub that I set up has a full working example. Try to reference that project and see if it fixes any issues you’re having.

    GitHub Project URL:
    https://github.com/hessler/iOS-Web-View-Multiwindow

  • Mithlesh Kumar Jha says:

    Hi Anthony,

    Your article gives good solutions but I found it not working with below url which is a straightforward window.open test url. Any ideas why?

    @”http://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_win_open”

  • Looks like that W3 Schools URL uses an iframe to display the “result” window. The solution for overriding and handling window.open events doesn’t factor in to handle iframes — it just injects the JS code into the main page, so any action taken in an iframe is outside the scope of the JS override functionality.

  • Mithlesh Kumar Jha says:

    Many thanks for the reply. I understand handling window.open in iFrame is outside the scope of JS code injection to main page. Can we, by any means, extend javascript override functionality to iFrame?

  • Mithlesh,

    Per the “Update: June 10, 2014” section above, I did some investigating and testing, and came up with a solution that loops through all frames on a page and attempts to override the appropriate window methods in each. The link to the Git repo with full source code is available above.

    Hope it helps, and thanks for the nudge to get the code updated for iframe support!

  • Mithlesh Kumar Jha says:

    This surely helped me. Thanks a lot Anthony!

  • Victor Engel says:

    window.open has a return value (the window). You should return a value in your replacement function or else sites that use the return value will not work.

  • Victor Engel says:

    Your replacement function should have a return value, else code like this will fail:

    someVar = window.open(foobar);
    someVar.focus(); //or whatever

  • Victor,

    Thanks for the comments. The override method used in this code for iOS doesn’t currently need to reference or return the new window, since it simply creates an iframe with a specific URL scheme that the iOS code catches and acts upon to create a new UIWebView. However, I have updated the code to include a simple return statement at the end of the window.open method, which should help any code that would otherwise fail with the lack of a return value.

    As noted in the article, I have not yet been able to figure out successfully handling window-to-window JavaScript communication – such as having a popup window call a method in the parent window. Incorporating this type of window-to-window communication would probably entail listening for all sorts of window methods (such as “window.parent.doSomething()”), which would need to be added in a dynamic way that presently I don’t have a good idea for tackling.

    Perhaps returning a more verbose window object would be a step in the right direction for supporting window-to-window communication, but I think that in its present state, returning the window object isn’t 100% necessary since there isn’t even a window created in the window.open override method.

    Again, thanks for the comments!

  • Mithlesh Kumar Jha says:

    Hi Anthony,

    I’ve another question related to opening the web page in new tab when window.open call is initiated from within JQuery function. Is it supported in the solution you provided here?

    I tried to test the solution with Jquery functions used in javascript functions but seems something is missing and am not getting below two lines of code working on desktop browser or mobile browser. As the result, no action is generated on tapping Try It button on the webpage. Any help is much appreciated.

    var url = ‘http://google.com’;
    window.open(url,”newW”,”location=no,resizable=yes,toolbar=no”);

    Click the button to open a new browser window.

    Try it

    function myFunction()
    {
    $.get(“http://www.apple.com”, function(req){
    var url = ‘http://google.com’;
    window.open(url,”newW”,”location=no,resizable=yes,toolbar=no”);
    });
    }

    Visit W3Schools!

  • Mithlesh,

    I don’t think your issue is anything with the window.open JS override, but rather in the use of the jQuery “get()” function. That function doesn’t call window.open() in and of itself, but loads data by performing a GET request.

    Using your sample code, I noticed the browser console was populated with the following error, which is why nothing happens when you click the button:

    “XMLHttpRequest cannot load http://www.apple.com/. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin is therefore not allowed access.”

    That said, it looks to be an issue with cross-domain requests as a result of the jQuery.get() function. If you need more info on the .get() function, I’d suggest reading up on it at its jQuery documentation page:
    http://api.jquery.com/jquery.get/

  • footyapps27 says:

    Hey Anthony,

    I would like to point one important point in this context.

    If the “link to load” is dynamically created by the HTML, then this will not work.

    So what exactly do I mean by “dynamically created”. Well suppose on tap of a button we are creating a form & posting it.
    Then this method will fail.
    Reason being that the form or HTML “a href” tag is not present in DOM at the time the webview completes loading.

  • footyapps27,

    Thanks for the comment. You’re absolutely right, dynamically created/loaded content is immune to the JS injection into the web view. Truthfully, I had not thought of dynamically created content being affected by this, as I didn’t have a need for including it in my original web view project.

    If there’s need to inject the JS and have it affect dynamically loaded content, you could look into Mutation events (https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events) and add in the necessary event listeners and event handlers that would re-inject the appropriate JS logic after the DOM has been updated.

  • Brad says:

    Can you provide the app this code was used in? I’d love to try out the multiple window handling functionality. Does the app allow you to enter your own URLs? Would like to try this on one of our sites to see how it handles pdfs, etc.

    Thanks

  • Brad,

    Unfortunately I cannot provide/show the app where this code was originally used, as it was for an internal project that is only available for select individuals – not a public app. I can say though, that the app did not allow the entering of your own URLs. It simply automatically opened a new window (w/o the user really knowing it) when necessary. It was not a full-fledged browser-like implementation.

  • Martin Swacke says:

    Hi Anthony, I tried your code in my app. I observe the following behaviour:
    1. Open page http://nasze-kino.eu
    2. Click on Bridge of Spies / Most Szpiegov
    3. Event is fired and caught in my shouldStartLoadWithRequest – popup is not open
    4. If I click again on the same link then it is not open. I need to wait about 30 seconds in order to get link back to work.

    Why is that?

    Thanks,
    Martin

  • Martin Swacke says:

    Hi, just to let you know I found the reason for this was that the website checks if window.open returns a valid object. I fixed it by changing “return;” in windowOpenFunction to “return window;”. Hope this helps.

  • Martin,

    Thanks for the comments, and specifically the follow-up. I didn’t have a chance to investigate the issue before you replied about the fix, but am glad to see it’s working for you. Cheers!

  • Hello Anthony,

    Good Article. I know you have not done any window to window communication yet. I have this kind of problem. As I have to popup facebook login popup from parent webview. After getting successful login from facebook, I have to pass those information to parent webview so it will do its further task. Please let me know if you are having any clue on this.

    Thanks,
    Paresh

  • Paresh,

    Admittedly I don’t have any insight into your question, as the issue of window-to-window communication is a bridge I haven’t been able to get over yet. Sorry I couldn’t be more help at the moment.

Leave a Comment