iPhone SDK: UIWebView to stop images from loading/downloading - iphone

How can I use the UIWebView in Xcode so that when it loads up pages it DOESN'T download the images (to cause a faster loading page)?

UIWebView is a pale, poor little shadow of WebKit's full WebView, for which this is easy. -webView:shouldStartLoadWithRequest:navigationType: only gets called for navigation. It doesn't get called for every request like WebPolicyDelegate does on mac. With UIWebView, here's how I would attack this problem:
Implement -webView:shouldStartLoadWithRequest:navigationType: and set it to always return NO. But you'll also take the request and spawn an NSURLConnection. When the NSURLConnection finishes fetching the data, you're going to look through it for any IMG tags and modify them to whatever placeholder you want. Then you will load the resulting string into the UIWebView using -loadHTMLString:baseURL:.
Of course parsing the HTML is not a trivial task on iPhone, and Javascript loaders are going to give you trouble, so this isn't a perfect answer, but it's the best I know of.

expanding on Rob's answer.
I noticed that when loadHTMLString:baseURL: and always returning NO, that webView:shouldStartLoadWithRequest:navigationType: just keeps getting called. (i suspect loadHTMLString invokes another shouldStartLoadWithRequest).
so what I had to do was alternate between returning YES/NO
and I used NSScanner to parse the HTML and change src="http://..." to src=""
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (pageHasNoImages==YES)
{
pageHasNoImages=FALSE;
return YES;
}
NSString* newHtml;
NSString* oldHtml;
NSData *urlData;
NSURLResponse *response;
NSError *error;
urlData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
oldHtml=[[NSString alloc] initWithData:urlData encoding:NSUTF8StringEncoding];
newHtml=[self.detail scannerReplaceImg:oldHtml]; // my own function to parse HTML
newHtml=[self.detail scannerReplaceAds:newHtml]; // my own function to parse HTML
if (newHtml==nil)
{
NSLog(#"newHtml is nil");
newHtml=oldHtml;
}
[oldHtml release];
pageHasNoImages=TRUE;
[web loadHTMLString:newHtml baseURL:request.URL];
return NO;
}

Be the delegate for the UIWebView, then intercept the call:
– webView:shouldStartLoadWithRequest:navigationType:
Check the values of navigationType in the documentation. I believe you'll be best served by returning NO on navigationType == UIWebViewNavigationTypeOther.

does this actually cause the page to load faster?
it sounds like the images are still being downloaded, but we're just not feeding them to the UIWebView.
or does shouldStartLoadWithRequest just load the HTML text first?

Related

get UIWebView's success loading response

I want to read the data in the UIWebView response when it succeeded loading the url.
Catching this data in this method didn't give me the response:
- (void) webViewDidFinishLoad:(UIWebView *)_webView {
EDIT:
I want to catch the NSURLResponse
Thanks,
Nur
Have you implemented –webView:didFailLoadWithError: ?
Probably an error when loading the URL.
I don't know enough about the architecture of your app to really say, but it might be useful for you to know that you can call arbitrary JavaScript on the page in your UIWebView from outside the UIWebView, and get the response back:
NSString *script = #"$('#textInputField').value";
NSString *result = [webView stringByEvaluatingJavaScriptFromString:script];
If you store the response in a JavaScript variable, you should be able to read it back up into your Obj-C this way.

UIWebView Still load the URL even though returning NO

I am handling a UIWebView so that i can control which URLs should be loaded within or not, but some how even though it is retuning the NO , it still load the page. Although documentation clearly says that if you return NO, the UIWebView wont load the page.
When i debug,i can see it is returning NO but still UIWebView does load the URL.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSLog(#"%#", [[request URL] absoluteString]);
NSString *fullURL = [[request URL] absoluteString];
NSRange range = [fullURL rangeOfString:#"#"];
if (range.length != 0) {
NSLog(#"We need to show the other view");
return NO;
}
return YES;
}
I solved the issue, documenting here so it may help someone else. Actually, the HTML, which we were loading using some javascript which was causing this issue. I found out by just using few plain html and testing with them. Once , we know the HTML is issue, we fixed the html and its working now.
Set a breakpoint or NSLog right before the return YES part. Maybe your method gets called twice for whatever reason, and it returns NO on one, and YES on the other.
First, make sure that you are setting the delegate in viewDidLoad with
webView.delegate = self;
(take care of not setting it 2 times, in a xib file and in viewDidLoad, it has caused me problems before)
Make sure you implement the UIWebViewDelegate in your class, something like this:
#interface RootViewController : UIViewController<UIwebViewDelegate>
Assuming you have taken care of all this and still you face problems. Also since you debugged and are SURE that the delegate method is returning NO. One reason I can think of as to why this is happening is that you are not loading a new page but using something like AJAX.
I tested the following code on 2 kinds of pages:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"THE loadCount is %d", self.loadCount);
if (self.loadCount > 1){
return NO;
}
self.loadCount++;
return YES;
}
Case 1: A webpage where every URL loads a new page. The above code works in this case, and I cannot load any pages after the first load, as required.
Case 2: A webpage in which the first load is a complete new web page. But everything else is loaded with AJAX, in that case, my loadCount does not increase and the pages load fine.
That's all I can think of with the data provided. :)
Same issue here, just to extend the answer above be careful when using rails 4 as backend because turbolinks add javascript to every link and then you can get wrong behavior on your delegate, happened to me returning NO on shouldStartLoadWithRequest and still see the request on my server.

What is the best (safest) way to augment the URL used by UIWebView?

We are developing a hybrid application for iOS. Most of the application is a WebApp running within a UIWebView. It is sometimes necessary for the native portion of the application to modify the URL, adding some parameters. I'm aware there are "alternate" solutions we could use (e.g. native app talks to our server independently of the webview); however, I'm primarily interested in a good & safe solution to URL augmentation.
I've been looking for the "best" technique to do this, and found two that appear to (mostly) work.
Technique #1 - re-initialize the URL
- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSLog(#"GOTO: %#", [[request URL] absoluteString]);
NSURL * oldurl = request.URL;
[[request URL] initWithString:[NSString stringWithFormat:#"%#&newParam=dave", [oldurl absoluteString]]];
return YES;
}
Technique #2 - reject the 1st URL, and sub-in a new one
- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL * oldurl = request.URL;
if ([self isFixed:oldurl]) {
return YES;
} else {
NSURL * newurl = [NSURL URLWithString:[NSString stringWithFormat:#"%#&newParam=dave", [oldurl absoluteString]]];
[theWebView loadRequest:[NSURLRequest requestWithURL:newurl]];
return NO;
}
}
Technique #2 seems to cause artifacts on some pages ("iFrame load interrupted", maybe other issues).
I'm curious if anyone else has done url replacement like this, or via another technique, or if anyone is is aware of problems with technique #1 (which I'll hopefully use).
I know method swizzling is not recommended, and Apple's document recommends against subclassing UIWebView.
Thanks!
UPDATE
Whoops - technique #1 doesn't actually work. My actual code was doing more than I posted, and it only appeared to work... This is probably good news, since it was a little more evil that I'd like.
Never call an -init method on an object that has already been initialized. This is not defined behavior. It is quite likely that you are leaking memory here since the old request ivars are probably not released correctly.
Something similar to the second approach is generally preferred.
Another approach is to use NSURLProtocol to inject yourself into the URL loading process. Doing it that way, you can create your own NSURLConnection using a modified NSURLRequest.
Here's another way to try out. I was playing with the stringByEvaluatingJavaScriptFromString:, and found out I could do this:
- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *oldUrlString = [NSString stringWithFormat:"%#", request.URL.absoluteURL];
if ([oldUrlString isEqualToString:#"http://www.google.com/"])
{
[webView stringByEvaluatingJavaScriptFromString:#"window.location = \"http://www.arstechnica.com\""];
return NO;
}
return YES;
}
I managed to redirect using a javascript injection, the first request to www.google.com is rejected, and redirected to www.arstechnica.com (I just picked a random site).
I think it's slightly better than hot-swapping the URL on the fly.

Update UI when using stringWithContentsofurl

I am using stringWithcontentsofurl to download some strings from my web server to the App, but i would like to update the UI to show A loader of some kind. I am downloading a number of strings (it can be sometimes as much as 100) so it would be neat for the user to show something so they know the App isn't crashing, because now the UI is stuck, i can't show A UILoader or something like that. Is there an option to do so? Or maybe A alternative to stringWithcontentsofurl where this is possible?
greets,
Erik
Try this. It loads stuff in background. If updating the UI later (in asyncload), be sure to do that on main thread.
-(void)loadappdetails:(NSString*)appid {
NSString* searchurl = [#"https://itunes.apple.com/lookup?id=" stringByAppendingString:appid];
[self performSelectorInBackground:#selector(asyncload:) withObject:searchurl];
}
-(void)asyncload:(NSString*)searchurl {
NSURL* url = [NSURL URLWithString:searchurl];
NSError* error = nil;
NSString* str = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
if (error != nil) {
NSLog(#"Error: %#", error);
}
NSLog(#"str: %#", str);
}
This is a classic case for "Lazy Loading". See Apple's example code:
http://developer.apple.com/library/ios/#samplecode/LazyTableImages/Introduction/Intro.html
It should be easy to substitute "strings" for "images" as you read that code.
You want to display a placeholder in the label like "(fetching information)" or similar and then load the information in the background, either threading the fetches yourself (asynchronous fetching) or using a library like ASIHTTPRequest that handles all the asynchronous nuts and bolts for you, calling a delegate method once the fetch has completed.
Unfortunately, stringWithContentsOfURL is a synchronous method, meaning it will block your thread and you will not receive any callbacks while it is running. This is also bad for user experience.
The alternative would be to use NSURLConnection to manually setup your own request, and tap into the delegate methods of your connection to display some sort of progress bar. Specifically, you'll want to use connection:didReceiveData: and connectionDidFinishLoading:
Once all the data has been received, use the following to get your string.
NSString *theString = [[NSString alloc] initWithData:yourData encoding:UTF8StringEncoding];
To make this method asynch it is possible and recommendable to use Grand Central Dispatch. If anyone is interested I will show the few lines of code to do that.

saving image in UITableViewCell

I am loading an image from an API in my tableviewcell, it downloads the image everytime I scroll down the UITableView. So what is the best way to save this image and so it doesn't have to download the image again if it is already there?
Same thing for text, how can I save the text...
If it's not very big, you can download it once and save it into user preferences (NSUserDefaults) as NSData object. Works for me.
Alternatively, you can use asynchronous requests with NSUrlConnection and implement caching in any way you like. (For example, update image only once a week.)
Moreover, even default cache settings of NSUrlConnection might work good enough.
More on caching
edit
An example of asynchronous request.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString: url]];
URLConnectionDelegate *delegate = ...;
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
delegate:delegate];
if (!connection) {
// something went wrong
}
In delegate, you mainly need methods to handle received data and to finish connection.
Assume you have NSMutableData *receivedData object.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// success, do whatever you want with data
[receivedData release];
[connection release];
}
The link above and API documentation provide more details about delegate structure.
Look at the LazyTableImages sample application in the iOS site sample code section. It has a great example of how to download images for a table cell using NSUrlConnection asynchronous calls and storing the images (and text) in an NSMutableArray.
This sample demonstrates a multi-stage
approach to loading and displaying a
UITableView. It begins by loading the
relevant text from an RSS feed so the
table can load as quickly as possible,
and then downloads the images for each
row asynchronously so the UI is more
responsive.