I want to set the back / forward button for my web view, so I check for canGoBack, canGoForward status in all 4 of the web view delegate methods. The delegate is set properly, and the methods are called in most cases. However, in a few cases they aren't called:
When I call [webView goBack] (for a certain links)
When I click to a link, go back, then click that link again (again, for a certain links).
I suspect it has something to do with the jquery mobile library or the html/css used in that page. However, Safari back button works properly.
So how can I track the back / forward button status, apart from making a timer?
Maybe you're missing webViewDidFinishLoading reset?
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
webBack.enabled = [webView canGoBack];
webForward.enabled = [webView canGoForward];
}
Related
My app features content that (for text formatting reasons) is presented in an UIWebView. Within the content there are links, some of which should open their target in mobile Safari, while others should navigate within the content.
So far, I've catched the link requests using a UIWebView delegate. In my implementation of
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
I'd check the requests URL using lastPathComponent or pathComponents for known elements to determine whether to open the link externally or within the view.
However, I just found out said methods are only available since iOS 4.0, which would render the app useless on iPad. Plus I have the feeling I'm using a dirty solution here.
Is there another way to somehow "mark" the links within my content in a way that makes them easy to distinguish later, when processing the request in the delegate method?
Thanks alot!!
You could covert the URL request into a string, and do a compare for a subdirectory on your website, such as in URLs that only start with "http://www.sample.com/myapp/myappswebcontent/", against the initial substring of your URL. Anything else, send to Safari.
You should set a policy delegate of web view:
For instance in the controller, that contains a web view
[webView setPolicyDelegate:self];
and then override a decidePolicyForNavigation method (this is just an example):
- (void)webView:(WebView *)sender decidePolicyForNavigationAction: (NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id <WebPolicyDecisionListener>)listener
{
if ([[actionInformation objectForKey:WebActionNavigationTypeKey] intValue] == WebNavigationTypeLinkClicked) {
[listener ignore];
[[NSWorkspace sharedWorkspace] openURL:[request URL]];
}
else
[listener use];
}
you can distinguish there kind of link and ignore or use the listener. If you ignore it, you can open the link in safari, if you use it, the link will open in your webview.
HTH
I have a UIWebView and a UITextField for the url. Naturally, I want the textField to always show the current document url. This works fine for urls directly input in the field, but I also have some buttons attached to the view for reload, back, and forward.
So I've added all the UIWebViewDelegate methods to my controller, so it can listen to whenever the webView navigates and change the url in the textField as needed.
Here's how I'm using the shouldStartLoadWithRequest: method:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSLog(#"navigated via %d", navigationType);
//loads the user cares about
if ( navigationType == UIWebViewNavigationTypeLinkClicked
|| navigationType == UIWebViewNavigationTypeBackForward ) {
//URL setting
[self setUrlQuietly:request.URL];
}
return YES;
}
Now, my problem here is that an actual click will generate a single navigation of type "LinkClicked" followed by a dozen type "Other" (redirects and ad loads I assume), which gets handled correctly by the code, but a back/forward action will generate all its requests as back/forward requests.
In other words, a click calls setUrlQuietly: once, but a back/forward calls it multiple times.
I am trying to use this method to determine if the user actually initiated the action (and I'd like to catch page redirects too). But if the method has no way of distinguishing between an actual "back" and a "load initiated as a result of a back", how can I make this assessment?
Without this, I am completely stumped as to how I can only show the actual url and not intermediate urls. Thank you!
Ok here's what I ended up doing - not sure if this is the expected solution though. Scratch all the code from the above method and instead do this:
- (void)webViewDidStartLoad:(UIWebView *)webView {
NSString *urlString = [self.webView stringByEvaluatingJavaScriptFromString:#"document.URL"]
[self setUrlQuietly:[NSURL URLWithString:urlString]];
}
This means there's a slight delay between clicking a link etc and seeing the url show up in the textField, but it also guarantees that the textField always shows the document's actual title.
I'm not seeing the UIWebViewNavigationTypeBackForward event at all. And sometimes webViewDidStartLoad never gets called. Perhaps there have been some changes in iOS 5.
Anyway, here's my solution for keeping the title and URL up to date. Works very consistently, though the response could be a little faster.
- (void)webViewDidFinishLoad:(UIWebView *)wv
{
[self updateButtons];
titleLabel.text = [wv stringByEvaluatingJavaScriptFromString:#"document.title"];
NSURL *url = wv.request.URL;
[selectedUrl release];
selectedUrl = [url retain];
}
I am designing an app which will present large amounts of text that is interspersed with notes and references as clickable images. On a PC I'd use a control that shows HTML, but in the iPhone I am not able to intercept the touches of images and links too well using the UIWeb control.
Should I use a UIScroll and build the text as lables and UIImages perhaps?
Looking for the best way forward in my design phase.
I don't know what your requirements are obviously, but it is possible to capture the click on an link in a UIWebView and take some alternative action. In one of my Apps, I have a UIWebView with one particular link which I want to route differently, while I let all other links open as web pages in the UIWebView as normal. Here's the code snippet from the app which accomplishes this. It is within a UIViewController which loads the UIWebView:
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [ request URL ];
if( [[url path] isEqualToString:#"/the_special_link.html"] ) {
// Take some alternative action and then stop the page from loading...
// (code to take some special action goes here)
return NO;
}
else {
return YES;
}
}
This is a delegate method call, so when I set up the UIWebView, which I do programmatically in the Controller loadView method, I set the WebView's delegate to that same Controller:
myWebView.delegate = self;
In one of my apps I reuse a webview. Each time the user enters a certain view on reload cached data to the webview using the method :-
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL
and I wait for the callback call
- (void) webViewDidFinishLoad:(UIWebView *)webView.
In the mean time I hide the webview and show a 'loading' label.
Only when I receive webViewDidFinishLoad do I show the webview.
Many times what happens I see the previous data that was loaded to the webview for a brief second before the new data I loaded kicks in.
I already added a delay of 0.2 seconds before showing the webview but it didn't help.
Instead of solving this by adding more time to the delay does anyone know how to solve this issue or maybe clear old data from a webview without release and allocating it every time?
Thanks malaki1974, in my case I wasn't using a modal view.
When I sat with an Apple engineer on WWDC 2010 and asked him this question his answer was simply: "Don't reuse UIWebViews, that's not how they were ment to be used."
Since then I make sure to calls this set of lines before allocating a new UIWebView
[self.myWebView removeFromSuperview];
self.myWebView.delegate = nil;
[self.myWebView stopLoading];
[self.myWebView release];
That solved the issue.
Clear the contents of the webview before you try to load new content
[self loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"about:blank"]]];
First, the UIWebView renders it contents in a background thread. Even when you receive webViewDidFinishLoad: it might not be completely done. Specially if it is an ajax-intense page that comes from the network.
You say you are hiding the view. I wonder if that means that the webview delays its drawing completely. What you could try is to move the UIWebView offscreen or obscure it with another view. Maybe that will change it's drawing behaviour.
If you do not need an interactive UIWebView then you can also consider to do it completely offscreen in a separate UIWindow and then create an image from that UIWebView's layer.
That's what I do, and it works:
[_webView stringByEvaluatingJavaScriptFromString:#"document.open();document.close();"];
Try loading a local file that is blank or has a loading graphic when you hide it, rather than just loading new content when you show it. Since the file is local it will be quick and even if the new page takes a while to load it will have either blank or loading expected behavior.
If you got controll over the html. You can communicate back to objective-c when the document is ready. Like so in jQuery:
function messageNative (name, string) {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "appscheme://" + name + "/" + string);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
$(function() {
messageNative('webview', 'ready');
});
And then in UIWebView's delegate method webView:shouldStartLoadWithRequest:navigationType: wait for the request with url equal to "appscheme://webview/ready". Then you should know: the document is loaded and ready for display. Then all that is missing is a simple fade-in or something like that :)
I have some code that needs to run after the a UIWebView finishes loading a document. For that I've set the UIWebView's delegate to my controller, and implemented the webViewDidFinishLoading method.
This gets called multiple times, depending on the type of page to load. I'm not sure if it's because of ajax requests, requests for images, or maybe even iframes.
Is there a way to tell that the main request has finished, meaning the HTML is completely loaded?
Or perhaps delay my code from firing until all of those events are done firing?
You can do something like this to check when loading is finished. Because you can have a lot of content on the same page you need it.
- (void)webViewDidFinishLoad:(UIWebView *)webview {
if (webview.isLoading)
return;
// do some work
}
It could be enlightening (if you haven't gone this far yet) to NSLog a trace of load starts and finishes.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSLog(#"Loading: %#", [request URL]);
return YES;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"didFinish: %#; stillLoading: %#", [[webView request]URL],
(webView.loading?#"YES":#"NO"));
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"didFail: %#; stillLoading: %#", [[webView request]URL],
(webView.loading?#"YES":#"NO"));
}
I just watched the calls to all three in one of my projects which loads a help page from my bundle and contains embedded resources (external css, YUI!, images). The only request that comes through is the initial page load, shouldStartLoadWithRequest isn't called for any of the dependencies. So it is curious why your didFinishLoad is called multiple times.
Perhaps what you're seeing is due to redirects, or as mentioned, ajax calls within a loaded page. But you at least should be able balance calls to shouldStartLoad and either of the other two delegate functions and be able to determine when the loading is finished.
Check this one it so simply and easy way to achieve no need to write too much code:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if ([[webView stringByEvaluatingJavaScriptFromString:#"document.readyState"] isEqualToString:#"complete"]) {
// UIWebView object has fully loaded.
}
}
This question is already solved, but I see it lacks an answer that actually explains why multiple calls to webViewDidFinishLoad are actually expected behavior
The aforementioned method is called every time the webview finishes loading a frame. From the UIWebViewDelegate protocol documentation:
webViewDidFinishLoad:
Sent after a web view finishes loading a frame.
In fact, this is also true for all the other methods that comprise the UIWebViewDelegate protocol.
Try this it will work fine
-(void)webViewDidFinishLoad:(UIWebView *)webview
{
if (webview.isLoading)
return;
else
{
// Use the code here which ever you need to run after webview loaded
}
}
This happens because the callback method is called every time a frame is done loading. In order to prevent this set the "suppressesIncrementalRendering" property of the webview to true. this will prevent the webview from rendering until the entire data is loaded into the memory. This did the trick for me
I have notice something similar and it was a confusion: I have a UITabBarController, it seems to preload all ViewControllers linked to its tabs on launching the App (in spite of showing just first_Tab_ViewController), so when several tabs have ViewController with WebView their respective webViewDidFinishLoad are called and if I have copied pasted:
NSLog(#"size width %0.0f height %0.0f", fitingSize.width, fittingSize.height);
in several, I get several output in console that appears to be a double calling when they really are single calling in two different UIWebViews.
You could check the loading and request properties in the webViewDidFinishLoad method
Possibly related to this issue is a property on UIWebView introduced in iOS6: suppressesIncrementalRendering.