I had a unique problem today – if a page opens a new ‘window’ in a UIWebView and then calls window.close to close it, how is that handled in iOS?
UPDATE: Swift version added at the bottom.
On top of that, the pop-up window was to allow the user to select a value for the opening page. That relationship was being lost.
So the two key issues are:
- Closing the opened page when window.close is called.
- Having the opened page set values on the opener page.
Let’s tackle Issue #1 first…
Create an instance member of:
UIWebView *wvPopUp;
This is where we’ll store the pop-up window instance. It also serves as a flag to know if we’re displaying another page.
In your shouldStartLoadWithRequest: method we check the URL and handle somethings…
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; { NSLog(@"request: %@", request.URL.relativePath); NSString *url = request.URL.absoluteString; NSLog(@"url: %@", url); // if we are selecting start or end time... if ([url rangeOfString:@"<something to detect your URL>"].location != NSNotFound) { // 3. time has been selected - close the pop-up window if ([url rangeOfString:@"back"].location == 0) { [wvPopUp removeFromSuperview]; wvPopUp = nil; return NO; } // 2. we're loading it and have already created it, etc. so just let it load if (wvPopUp) return YES; // 1. we have a 'popup' request - create the new view and display it UIWebView *wv = [self popUpWebview]; [wv loadRequest:request]; return NO; } return YES; }
In the shouldStartLoadWithRequest: method we check to see if we’re handling a pop-up related URL. This may not work if you don’t know what URLs cause a popup for you. More on that later.
They occur in reverse order, but we need to check it in that way. It should be clear as we go…
A. We create the new UIWebView (see below, stored in wvPopUp) and load the current request.
B. If we have a wvPopUp, it means we’re trying to load the new request in the web view from step 1 so return YES.
C. If we’re trying to close the popup, remove it and set to nil. We’ll see in a bit where ‘back’ comes from.
Let’s look at where we create the web view for the pop-up in step A…
- (UIWebView *) popUpWebview { // 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)]; webView.scalesPageToFit = YES; webView.delegate = self; // Add to windows array and make active window wvPopUp = webView; [self.view addSubview:webView]; return webView; }
This is fairly straight forward – alloc-init the web view, set some values and store it in wvPopUp. You can add it to the view here or return it and do it.
In our step B, we just pass thru the loading of the new popup page.
In order to detect when the ‘window.close’ is called in our step C above, we’ll add some JavaScript to our pages via the webViewDidFinishLoad: method called via the delegate pattern.
- (void)webViewDidFinishLoad:(UIWebView *)webView; { // this is a pop-up window if (wvPopUp) { // overwrite the 'window.close' to be a 'back://' URL (see above) NSError *error = nil; NSString *jsFromFile = [NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"JS2" withExtension:@"txt"] encoding:NSUTF8StringEncoding error:&error]; __unused NSString *jsOverrides = [webView stringByEvaluatingJavaScriptFromString:jsFromFile]; } }
If we have a popup at this point, that’s the one that finished loading. We read in some JavaScript from a file named JS2.txt in our app and add it to our web view. Here’s the JavaScript in that file…
window.close = function () { window.location.assign("back://" + window.location); };
All this does is overwrite the window.close function by changing it to a location assign and prepend the current window.location w/ “back://”. This does three things for us: 1) it creates the call shouldStartLoadWithRequest: and 2) it tells us the user wants to go back and 3) from where.
So at this point, you have the shouldStartLoadWithRequest: which detects the url, creates the new web view, loads it and displays it. When it finishes loading, it gets the window.close overwrite code which sends the ‘back://’ URL when it closes. Our shouldStartLoadWithRequest: gets called again and the ‘back’ is detected to close the window.
That handles displaying and closing the pop-up. But what about that pop-up allowing the user to select a value and setting it.
Issue #2:
Typically, that’s done by the pop-up page using window.opener.document calls but we don’t have that established in our web view. In our ‘if (wvPopUp)’ block above in webViewDidFinishLoad: we can add code for that relationship. Now it looks like…
import <JavaScriptCore/JavaScriptCore.h> // this is a private framework pre-iOS7 ... - (void)webViewDidFinishLoad:(UIWebView *)webView; { // this is a pop-up window if (wvPopUp) { // overwrite the 'window.close' to be a 'back://' URL (see above) NSError *error = nil; NSString *jsFromFile = [NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"JS2" withExtension:@"txt"] encoding:NSUTF8StringEncoding error:&error]; __unused NSString *jsOverrides = [webView stringByEvaluatingJavaScriptFromString:jsFromFile]; // set the child's 'opener' so it can set the user selected value JSContext *openerContext = [self.wvMain valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; JSContext *popupContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; popupContext[@"window"][@"opener"] = openerContext[@"window"]; } }
What we added was the last 3 lines to get the contexts and set the opener in the popup context to the main context. I have self.wvMain for my web view from Interface Builder, but yours may be named something else.
This takes care of issue #2.
If you don’t know what’s causing a popup, you might need another window.open overwrite method to indicate that case and handle it. Something like…
window.open = function (url, d1, d2) { window.location = "open://" + url; };
However, the url being passed around may be relative. So your shouldStartLoadWithRequest: may have to massage that URL depending on how it’s called. But you don’t have to check for rangeOfString: for your URL anymore – just ‘back’ or ‘open’ (or whatever you use).
Your webViewDidFinishLoad: would need to load the JS2.txt file in the opening AND opened pages so unless you know what pages might open another page, you probably have to add it to all pages opened.
A reader, Jean-Sebastien Perron, sent me this Swift input:
let jsContextA = webA.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") let jsContextB = webB.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext")//Original objc code : jsContextB[@"window"][@"opener"] = jsContextA[@"window"]; jsContextB!.setObject("opener", forKeyedSubscript: "window") jsContextB!.setObject(jsContextA!.objectForKeyedSubscript("window"), forKeyedSubscript: "opener")
I haven’t verified the above Swift code.