UIWebView shouldStartLoadWithRequest only fires once when calling a modal view from inside - iphone

I have part of my app written in JS and running inside of a WebView. I'm using the UIWebView shouldStartLoadWithRequest method to capture http requests as a means of communicating between JS and obj-c. This works great until I attempt to load a Modal View Controller over my webview from inside the shouldStartLoadWithRequest method. Once this happens, shouldStartLoadWithRequest is no longer called. Sometimes I need to dismiss this modal view controller and go back to the webview and do some things and then re-present the modal controller. The modal controller comes up the first time just fine, then I dismiss it and attempt to present it again by navigating to a URL from javascript and it no longer will present itself. NSLogs inside shouldStartLoadWithRequest are never run.
In my javascript I do something like this:
window.location='myapp:whateverMethod';
objective c code looks like this:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *requestString = [[request URL] absoluteString];
NSLog(#"REQUEST URL: %#", requestString);
if([requestString hasPrefix:#"myapp:"]) {
NSArray *components = [requestString componentsSeparatedByString:#":"];
NSString *function = [components objectAtIndex:1];
if([self respondsToSelector:NSSelectorFromString(function)]) {
[self performSelector:NSSelectorFromString(function)];
}
return NO;
}
return YES;
}
-(void) whateverMethod {
NSLog(#"whateverMethod called!");
// This is a quick way to grab my view controller from the storyboard, so assume it exists
UIViewController *splash = [self.storyboard instantiateViewControllerWithIdentifier:#"splashViewController"];
[self presentModalViewController:splash animated:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[self dismissModalViewController:splash animated:NO];
});
}
At this point my webview is still visible. I navigate from page to page in my webapp and all javascript works great in it but the "shouldStartLoadWithRequest" delegate method of the webview no longer is called. I cannot figure out why. Does anyone have any ideas?

I noticed that Cordova doesn't set the window.location property. Instead it has two options: it either creates an iframe and sets the src of the iframe to that url, or it creates an XMLHttpRequest object e.g. in the iOSExec() function:
if (bridgeMode) {
execXhr = execXhr || new XMLHttpRequest();
// Changeing this to a GET will make the XHR reach the URIProtocol on 4.2.
// For some reason it still doesn't work though...
execXhr.open('HEAD', "file:///!gap_exec", true);
execXhr.setRequestHeader('vc', cordova.iOSVCAddr);
if (shouldBundleCommandJson()) {
execXhr.setRequestHeader('cmds', nativecomm());
}
execXhr.send(null);
} else {
execIframe = execIframe || createExecIframe();
execIframe.src = "gap://ready";
}
That being said, it may be beneficial to use something like Cordova instead of trying to roll it yourself (even if it's just embedding their view controller), since they handle a lot of the headaches that come up with webview delegates.

I've just had the same problem, but related to using a href="#" anchor.
This Stack Overflow answer sorted it
There are more answers on that thread that deal with widow.location, so you may have luck with them.

Checked out Cordova and they have their own queuing system, not really a help. But...
Disobedient Media's answer gave me an idea. Instead of window.location, why not try window.location.hash.
Now some JS code for logging is:
function sendMsgToNative(msg)
{
window.location.hash = '~cmd~' + msg;
}
console.log = function (msg) { sendMsgToNative('log~js ' + msg); };
and the Objective-C code is:
NSString *req = [request.URL absoluteString];
NSArray *components = [req componentsSeparatedByString:#"~"];
// Check for your protocol
if ([components count] > 1 && [(NSString *)[components objectAtIndex:1] isEqualToString:#"cmd"])
{
// Look for specific actions
if ([(NSString *)[components objectAtIndex:2] isEqualToString:#"log"])
{
NSString *logStr = [(NSString *)[components objectAtIndex:3] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
LOGI("%#", logStr);
}
}
You get the full URL including 'http:...' so I chose tilde instead of colon, and incremented the indices.
Now you can log all willy-nilly and send whatever amount of commands you want and they will all get through :-)

I (embarrassingly) spent a couple of hours working on this today, and realised that in my viewDidDisappear: I was setting the UIWebViewDelegate to nil!
All I needed to do to fix was once the modal was dismissed, re-set the UIWebViewDelegate and everything worked again.

Related

shouldStartLoadWithRequest doesn't run stringByEvaluatingJavaScriptFromString method

I am trying to run the code below in webview's shouldStartLoadWithRequest delegate method but it doesn't make any changes.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSLog(#"webView shouldStartLoadingWithRequest");
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"document.execCommand('bold', false,null)"]];
return TRUE;
}
There is no error, it prints the NSLog and everything in the method works great except "stringByEvaluatingJavaScriptFromString" method.
But if I try to make text bold in another function, for example an IBAction method, works fine.
-(IBAction)boldClick:(id)sender
{
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"document.execCommand('bold', false,null)"]];
}
Actually, this is my company's special application and this UIWebView will not show the web pages. I am using it to show some custom HTML pages.
I need to make everything in "shouldStartLoadWithRequest" because I am trying to run an objective-c method from javascript.
UPDATE
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
// Break apart request URL
NSString *requestString = [[request URL] absoluteString];
NSArray *components = [requestString componentsSeparatedByString:#":"];
// Check for your protocol
if ([components count]==3)
{
[self makeBoldText];
return NO;
}
else
{
return TRUE;
}
}
-(void)makeBoldText
{
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"document.execCommand('bold', false,null)"]];
}
The doc says that method webView:shouldStartLoadWithRequest: is sent before a web view begins loading a frame. After you return YES in this method, web view starts loading a request. So any javascript you execute will have no effect, because a new page will be loaded after your JS call.
You can probably use webViewDidFinishLoad: method to execute javascript after page finishes to load. Or if you want to trigger JS by clicking on a link, you can use shouldStartLoadWithRequest but return NO from it.

IOS 5 showing SVProgressHUD when loading feed in separate thread does not work

Im developing an IOS 5 app which takes a feed from a url and displays the posts in a tableview. I have a View controller that loads the table cells with the posts in the feed. This all works perfectly.
However, i wanted to use the SVProgressHUD to show whilst the feed is being loaded in a separate thread.
So in my -(void)viewDidLoad method I have the following:
- (void)viewDidLoad
{
[super viewDidLoad];
[SVProgressHUD showInView:self.view status:#"loading.." networkIndicator:YES];
dispatch_async(kBgQueue, ^{NSData* data = [NSData dataWithContentsOfURL: latestFeedURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];});
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(foregroundRefresh:) name:UIApplicationWillEnterForegroundNotification object:nil];
self.pull = [[PullToRefreshView alloc] initWithScrollView:(UIScrollView *) self.feedTableView];
[self.pull setDelegate:self];
[self.feedTableView addSubview:self.pull];
self.title = #"Latest";
}
- (void)fetchedData:(NSData *)responseData {
//parse out the json data
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
NSMutableArray* latestFeed = [json objectForKey:#"posts"]; //2
self.feedUpLoads = latestFeed;
NSLog(#"objects: %#", latestFeed); //3
[self.feedTableView reloadData];
[SVProgressHUD dismiss];
}
This all works fine, im getting the data which is loaded in a background thread and my table is displaying the posts with all the detail required. The problem I have is that the SVProgressHUD is not showing at all. Even if I put the [SVProgressHUD showInView line in the fetchData method, it's still not showing. (by the way i know the SVProgressHUD code works because I can actually make it show forexample in the viewWillAppear method.
Im guessing that it's not working because at the point when I'm calling it the view does not yet fully exist? But if that's the case where should I call it so that it shows whilst the feed is being called and where should I remove it?
Any help appreciated! thanks in advance!!
For anyone else having a similar problem, this can also happen because you have a long loop or a piece of code that takes a long time to execute. If this happens, your progress bar wont be shown until after the loop, which kind of defeats the purpose.
To solve this issue you need to you this:
(void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
Basically your code would look something like this:
- (IBAction)submitPost:(id)sender {
//now we show the loading bar and submit the comment
[SVProgressHUD showWithStatus:#"Submitting post" maskType:SVProgressHUDMaskTypeGradient];
SEL aSelector = #selector(submitDataOfPost);
[self performSelectorInBackground:aSelector withObject:sender];
}
This will basically load the progress bar, and in a background thread, the method you want to execute will be called. This makes sure that the UI is updated (shows the progress hud) at the same time that your code is executed.

Parse url so that only maps links open in external app from within UIWebView?

I have web content inside a UIWebView in my app. My goal is to have all links open normally inside the app, but any links starting with "http://maps", get opened in safari, so they can in turn be opened in the external iphone maps app. If you have a solution for this problem stop reading now, below I'm going to propose my solution. Currently all links are opened inside the app, so http://maps links open to m.google.com inside the app. The solution I'm thinking of involves this code which uses openURL to open all links in safari:
(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
\[\[UIApplication sharedApplication] openURL:request.URL];
return false;
}
return true;
}
Obviously the problem with this code is that all links are opened in safari, and I only want map links. Can you suggest a way to parse through links and only pass ones that start with http://maps through the function? Also a more simple question, how do I delegate UIWebView so I can run this code, and also is the viewcontroller.m the right place to put this code?
If you guys could suggest an entire function, including the openURL part above and the link parsing to make sure only maps links get passed through the function that would be awesome. Again, if you have another solution or workaround I would love to hear it. Thanks so much for your help, stackoverflow has been a lifesaver, I'm almost finished with my first project ever!
Try something like this:
-(BOOL)webView:(UIWebView *)inWeb
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)inType {
// let UIWebView deal with non-click requests
if (inType != UIWebViewNavigationTypeLinkClicked) {
return YES;
}
// URL starts with "http://maps"?
if ([[request.URL description] hasPrefix:#"http://maps"]) {
// open URL in Safari and return NO to prevent UIWebView from load it
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
// otherwise let UIWebView deal with the request
return YES;
}
NSString *linkPath = [[request url] path];
if ([linkPath hasPrefix:#""http://maps"]) {
//open in safari
}
else {
//do whatever
}

Opening popup links in UIWebView, possible?

I have a UIWebView which I'm using as an embedded browser within my app.
I've noticed that links in webpages that open new windows are ignored without any call into my code.
I've tried breakpointing on
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
and then selecting a link that would open a popup window, and the breakpoint is never hit.
Is there anything I can do to intercept that selection of the popup link and get the URL and just load it normally?
I'm not interested in displaying a popup window in the app itself, I just want the URL of whatever is going to be loaded in the popup window to load in the main webview itself.
Is this possible?
Thanks!
I ran into this as well, and HTML rewriting was the best solution I could come up with. The biggest issue that I ran into with that approach is that the web browser is interactive for up to a couple of seconds until the webViewDidFinishLoad: method is called, so the links seem to be broken for a few seconds until they're rewritten.
There's three areas that I rewrote: links, form posts, and calls to window.open().
I used a similar approach to the first code snipped in Jasarian's answer to overwrite the target for links and forms by iterating over tags and forms. To override window.open, I used code similar to the following:
var oldWindowOpen = window.open;
window.open = function(url, sName, sFeatures, bReplace) {
oldWindowOpen(url, '_self');
};
So after a small amount of research, it's clear that the UIWebView class purposefully ignores links that will open in a new window (either by using the 'target' element on the a tag or using javascript in the onClick event).
The only solutions I have found are to manipulate the html of a page using javascript. While this works for some cases, it's not bulletproof. Here are some examples:
links = document.getElementsByTagName('a');
for (i=0; i<links.length; i++)
{
links[i].target='_self';
}
This will change all links that use the 'target' element to point at _self - instead of _blank or _new. This will probably work across the board and not present any problems.
The other snippet I found followed the same idea, but with the onClick event:
links = document.getElementsByTagName('a');
for (i=0; i<links.length; i++)
{
links[i].onclick='';
}
This one is just plain nasty. It'll only work if the link tag has it's href element correctly set, and only if the onclick event is used to open the new window (using window.open() or something similar). The reasons why it is nasty shouldn't need explaining, but one example would be if the onClick is used for anything other than opening a window - which is a very common case.
I guess one could go further with this and start doing some string matching with the onClick method, and check for window.open(), but again, this is really far from ideal.
Here's how I get twitter links to work (i.e. link to pages that try to open with new windows):
-(BOOL)webView:(UIWebView *)mainWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (navigationType == UIWebViewNavigationTypeLinkClicked) {
//Allows for twitter links
[self.mainWebView loadRequest:request];
return NO;
}
return YES;
}
WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init];
theConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
webView1 = [[WKWebView alloc] initWithFrame:self.webView.frame configuration:theConfiguration];
webView1.navigationDelegate = self;
webView1.UIDelegate = self;
[self.view addSubview:webView1];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
NSString *htmlString = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(#"htmlString: %#", htmlString);
[webView1 loadHTMLString:htmlString baseURL:[NSURL URLWithString:#"your url"];
}];
[dataTask resume];

UIWebView loading parsed html string

i'm building an app that will display some newsletters to the users. the newsletters are displayed in a uiWebView. I'm reading the url's for the newsletters from an rss feed. I parse the xml, and in a table view i have all the newsletters. When a cell is clicked the uiWebView is pushed and the newsletter is loaded. Because the uiWebView doesn't open links that have target=_blank i need to replace the _blank from target with "". In an NSOperation i download the contents of the html and after the download is finished i replace the strings like this:
NSMutableString *page = [[[NSMutableString alloc] initWithData:pageData encoding:NSISOLatin1StringEncoding] autorelease]; [page replaceOccurrencesOfString:#"target=_blank" withString:#"target=""" options:0 range:NSMakeRange(0, [page length])];
after i do this i load the parsed string in the webView to display it to the user.
[myWebView loadHTMLString:page baseURL:[NSURL URLWithString:#""]];
while the NSOperation is downloading the contents of the page an HUD with a activity indicator and a label that says Loading is showed. When the download is finished the HUD is removed from the superview.
so far so good..but here come the questions. i used the NSOperation with the callback function because i wasn't able to determine the last call to webDidFinishLoading ( i read this UIWebView - How to identify the "last" webViewDidFinishLoad message? - this is a part of my problem but the response was to use some Apple private classes and that is a problem for me). so i'm doing this:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = request.URL;
NSString *urlString = url.absoluteString;
NSLog(#"WebViewSouldStartWithRequest: %#", urlString);
if (navigationType == UIWebViewNavigationTypeLinkClicked)
{
if(![urlString isEqualToString:#"about:blank"])
{
NSLog(#"FeedViewController: startPageLoad");
activityHud = [[HUDView alloc] initWithFrame:CGRectMake(110, 100, 80, 80)];
[activityHud setText:#"Loading"];
[myWebView addSubview:activityHud];
NSLog(#"FeedViewController: pageUrl = %#", urlString);
[self resetWebView];
[urlLoader loadPageFromUrl:request.URL withCallbackTarget:self withCallbackSelector:#selector(endLoading:)];
}
}
return NO;
}
- (void) endLoading:(NSMutableString *)page { [self resetWebView]; NSLog(#"FeedViewController: endLoading"); [activityHud removeFromSuperview]; [myWebView loadHTMLString:page baseURL:[NSURL URLWithString:#""]]; }
after i touch a cell in the table view and the newsletter is showed it looks like it should, when i click a link in the newsletter, the page is loaded with the new request, parsed but visually is not looking as it should (i looked at the page after parsing in NSLog and it has the css styles and html tags, head, body opened and closed correctly) anyone had this problem with uiWebView, not showing webpages correctly?
i tried loading the page with - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType{ return YES; }
and in - (void)webViewDidFinishLoad:(UIWebView *)webview {} and in - webViewDiStartLoad
but the methods are being called for every item that is loaded in the webview so showing and hiding the HUD in those method is not a good solution.
I encountered some problems while using - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { ... return NO }
for example links that have paths like this /bilder-galerien to not work and in NSLog i receive this for them
Unknown scheme, doing nothing:
/bilder-galerien
but when i use
(BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { return Yes } and didstartLoading and finishLoading the urls are loaded and i don't know why...
another problem are the special german characters..in my first load(after pressing a cell in the uiTableView) the page is parsed like it should be by the uiWebView but after i click a link and the corresponding request is loaded also the characters are not parsed correctly..
can anyone point me in the good direction? thank you in advance
UIWebView doesn't open target="_blank" links. Certain links don't fire UIWebViewNavigationTypeLinkClicked events. This happens when the link has the target="_blank" attribute.
To work around this problem i used the code below. It injects some javascript after the page load to remove the target attribute from all the links. After using this code i didn;t needed anymore to parse the html source and replace _blank with _self for example.
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSString *js = #"\
var d = document.getElementsByTagName('a');\
for (var i = 0; i < d.length; i++) {\
if (d[i].getAttribute('target') == '_blank') {\
d[i].removeAttribute('target');\
}\
}\
";
[webView stringByEvaluatingJavaScriptFromString:js];
}
I like SorinA answer, we can make it even better by:
avoid obstructing javascript with var d
as we know we're running webkit engine we can use selectors:
...so in your UIWebViewDelegate protocol method invoke js code:
- (void) webViewDidFinishLoad: (UIWebView *) webView {
[webView stringByEvaluatingJavaScriptFromString: #"(function($){for(var i=0;i<$.length;++i){$[i].removeAttribute('target')}})(document.querySelectorAll('a[target=_blank]'))"];
}