Cached images in UIWebView take longer to load? (NSURLProtocol?) - ios

I'm serving up local images to my UIWebView via NSURLProtocol (which means the image is returned almost immediately), but I'm experiencing an issue where cached images (images being displayed again after their first load) take longer to load. Is there something in my NSURLProtocol causing this?
#implementation URLProtocol
+ (BOOL) canInitWithRequest:(NSURLRequest *)request {
return [request.URL.scheme isEqualToString:#"file"] ||
[request.URL.scheme isEqualToString:#"http"];
}
+ (NSURLRequest*) canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
- (void) startLoading {
id<NSURLProtocolClient> client = self.client;
NSURLRequest* request = self.request;
NSString *fileToLoad = request.URL.absoluteString;
NSURLResponse *response;
if([fileToLoad hasPrefix:#"http://app-fullpath/"]){
fileToLoad = [fileToLoad stringByReplacingOccurrencesOfString:#"http://app-fullpath/" withString:#""];
} else {
fileToLoad = [[NSURL URLWithString:fileToLoad] path];
}
NSData* data = [NSData dataWithContentsOfFile:fileToLoad];
response = [[NSHTTPURLResponse alloc] initWithURL:[request URL] statusCode:200 HTTPVersion:#"HTTP/1.1" headerFields:[NSDictionary dictionary]];
[client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
- (void) stopLoading { }
#end
Any speed suggestions, javascript/html or iOS?

My problem was that UIWebView gives text a much higher priority than images, so text is laid out first, then images are processed. In order to fix that I created a DOM representation of my HTML & Images, then I replaced all images with images loaded via javascript (new Image()) and they show instantly.

Related

How to download a file from the webview in os x and save it to a specific location

I have looked everywhere and have not found exactly what I am looking for, so here is my question:
I have a basic app I am playing around with. I have created a webview and would like to be able to download a file from the website that loads in the webview and save the file to say the Downloads folder on the local machine. The site loads fine inside the webview, now how do I download a file, say an .xml file from the site and save it to the Downloads folder on the local machine?
This is what I have so far:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSURL *url = [NSURL URLWithString:#"http://www.google.com"];//<-- example site
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[webView mainFrame] loadRequest:request];
}
I would like to be able to download a file (possible using a delegate) then save it to a location on the local computer. I am pretty new to this so I'd appreciate any help.
The issue has been resolved. I added the following code to make it work:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSURL *url = [NSURL URLWithString:#"http://www.google.com"]; // <-- example website
NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.webView.policyDelegate = self;
[self.webView setDownloadDelegate:self];
[[self.webView mainFrame] loadRequest:request];
}
- (void)webView:(WebView *)webView decidePolicyForMIMEType:(NSString *)type
request:(NSURLRequest *)request
frame:(WebFrame *)frame
decisionListener:(id < WebPolicyDecisionListener >)listener
{
if([type isEqualToString:#"application/octet-stream"]) //this is the type I was looking for
{
//figure out how to save file here
[listener download];
NSURLDownload *downLoad = [[NSURLDownload alloc] initWithRequest:request delegate:self];
if(downLoad)
{
[self download:downLoad decideDestinationWithSuggestedFilename:#"filename.ext"];
NSLog(#"File Downloaded Succesfully");
//[webView close];
[self.window close];
}
else
{
NSLog(#"The download failed");
}
}
//just ignore all other types; the default behaviour will be used
}
-(void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)filename
{
NSString *destinationFileName;
NSString *homeDirectory = NSHomeDirectory();
destinationFileName = [[homeDirectory stringByAppendingPathComponent:#"Documents"] stringByAppendingPathComponent:filename];
[download setDestination:destinationFileName allowOverwrite:NO]; //file is being saved to the Documents folder on the local machine
}
Hope this will be helpful to someone else.

iOS 7 UIWebView 304 cache bug, blank pages

I have a problem I have discovered in my app that has a UIWebView. iOS 7 caches a blank body 304 response, resulting in blank pages being shown when the user refreshes the UIWebView. This is not good user expierience and I'm trying to figure out how to solve this on the iOS side, as I do not have control over how Amazon S3 responds to headers (that's who I use for my resource hosting).
More details of this bug were found by these people: http://tech.vg.no/2013/10/02/ios7-bug-shows-white-page-when-getting-304-not-modified-from-server/
I'd appreciate any help offered to how I can solve this on the app side and not the server side.
Thank you.
Update: fixed this bug using the bounty's suggestion as a guideline:
#property (nonatomic, strong) NSString *lastURL;
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if ([self.webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML"].length < 1)
{
NSLog(#"Reconstructing request...");
NSString *uniqueURL = [NSString stringWithFormat:#"%#?t=%#", self.lastURL, [[NSProcessInfo processInfo] globallyUniqueString]];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];
}
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
self.lastURL = [request.URL absoluteString];
return YES;
}
You can implement a NSURLProtocol, and then in +canonicalRequestForRequest: modify the request to override the cache policy.
This will work for all requests made, including for static resources in the web view which are not normally consulted with the public API delegate.
This is very powerful, and yet, rather easy to implement.
Here is more information:
http://nshipster.com/nsurlprotocol/
Reference:
https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Classes/NSURLProtocol_Class/Reference/Reference.html
Here is an example:
#interface NoCacheProtocol : NSURLProtocol
#end
#implementation NoCacheProtocol
+ (void)load
{
[NSURLProtocol registerClass:[NoCacheProtocol class]];
}
+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
if ([NSURLProtocol propertyForKey:#“ProtocolRequest” inRequest:theRequest] == nil) {
return YES;
}
return NO;
}
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
NSMutableURLRequest* request = [theRequest mutableCopy];
[request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData];
//Prevent infinite recursion:
[NSURLProtocol setProperty:#YES forKey:#"ProtocolRequest" inRequest:request];
return request;
}
- (void)startLoading
{
//This is an example and very simple load..
[NSURLConnection sendAsynchronousRequest:self.request queue:[NSOperationQueue currentQueue] completionHandler:^ (NSURLResponse* response, NSData* data, NSError* error) {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}];
}
- (void)stopLoading
{
NSLog(#"something went wrong!");
}
#end
As the other questions are to use the NSURLConnection every time, which seems like a bit of an overhead:
Why don't you execute a small javascript after the page was loaded (complete or incomplete) that can tell you if the page is actually showing? Query for a tag that should be there (say your content div) and give a true/false back using the
[UIWebView stringByEvaluatingJavaScriptFromString:#"document.getElementById('adcHeader')!=null"]
And then, should that return false, you can reload the URL manually using the cache-breaker technique you described yourself:
NSString *uniqueURL = [NSString stringWithFormat:#"%#?t=%d", self.url, [[NSDate date] timeIntervalSince1970]];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];
[edit]
based on the discussion in the comments and some of the other answers, I think you might have the best solution manually changing the NSURLCache.
From what I gathered, you're mainly trying to solve a reload/reshow scenario. In that case, query the NSURLCache if a correct response is there, and if not delete the storedvalue before reloading the UIWebView.
[edit 2]
based on your new results, try to delete the NSURLCache when it is corrupted:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache]cachedResponseForRequest:request];
if (cachedResponse != nil && [[cachedResponse data] length] > 0)
{
NSLog(#"%#",cachedResponse.response);
} else {
[[NSURLCache sharedURLCache] removeCachedResponseForRequest:request];
}
return YES;
}
We might have to refine the check if the cache is invalid again, but in theory this should do the trick!
NSURL *URL = [NSURL URLWithString:#"http://mywebsite.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:30.0];
[myWebView loadRequest: request];
When creating NSURLRequest instance, you can set Cache Policy.
Hope it works!

How to prevent the browser cache interfering with NSURLCache?

I am trying to achieve the following on iOS:
Always load local files to a UIWebView for static assets (.html, .js, etc.)
Allow an update protocol such that after some period of time we can return a different set of static assets for the same URLs
Download link for minimal example.
I have this working but it seems the NSURLCache is sometimes completely missed (reproducible) and the only reliable way to fix this has been to use nasty cachebusting tricks on the page being loaded. Another equally nasty hack is to destroy the UIWebView and create another.
As an example of this we have three versions of our webapp (red, blue and green for v1, v2 and v3 respectively):
Each screen is made up of a single HTML and JS file.
index.html
----------
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:#FF0000">
<h1 id="label" style="color:#FFFFFF"></h1>
<script src="app.js"></script>
</body>
</html>
app.js
------
var label = document.getElementById('label');
label.innerHTML = 'red';
Every 2 seconds the following happens:
We change what files the NSURLCache will return to make it return the different versions (note we implement this by overriding cachedResponseForRequest: rather than storeCachedResponse:forRequest:)
The UIWebView loads a dummy page "http://www.cacheddemo.co.uk/index.html"
The NSURLCache logic is implemented simply as a rotating NSMutableArray:
#implementation ExampleCache
- (id)init
{
self = [super initWithMemoryCapacity:8 * 1024 * 1024 diskCapacity:8 * 1024 * 1024 diskPath:#"webcache.db"];
if(self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(swapCache:) name:#"swapCache" object:nil];
self.cache = [#[#{#"index.html":#"index1.html", #"app.js":#"app1.js"},
#{#"index.html":#"index2.html", #"app.js":#"app2.js"},
#{#"index.html":#"index3.html", #"app.js":#"app3.js"}] mutableCopy];
}
return self;
}
- (void)swapCache:(NSNotification *)notification
{
[self.cache addObject:self.cache[0]];
[self.cache removeObjectAtIndex:0];
}
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSString *file = [[[request URL] pathComponents] lastObject];
NSString *mimeType;
if([file hasSuffix:#".html"]) {
mimeType = #"text/html";
} else if([file hasSuffix:#".js"]) {
mimeType = #"application/javascript";
}
if(mimeType) {
NSString *cachedFile = self.cache[0][file];
NSUInteger indexOfDot = [cachedFile rangeOfString:#"."].location;
NSString *path = [[NSBundle mainBundle] pathForResource:[cachedFile substringToIndex:indexOfDot] ofType:[cachedFile substringFromIndex:indexOfDot + 1] inDirectory:#"www"];
NSData *data = [NSData dataWithContentsOfFile:path];
if(data.length) {
NSLog(#"Response returned for %#", file);
NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
NSCachedURLResponse *response = [[NSCachedURLResponse alloc] initWithResponse:urlResponse data:data];
return response;
}
}
NSLog(#"No response for %# - %#", file, request);
return nil;
}
The view controller logic uses GCD to reload the UIWebView after the delay:
- (void)viewDidLoad
{
[super viewDidLoad];
self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[self.webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
[self.webView setDelegate:self];
[self.view addSubview:self.webView];
[self loadContent];
}
- (void)loadContent
{
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
// [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[NSNotificationCenter defaultCenter] postNotificationName:#"swapCache" object:nil];
[self loadContent];
});
}
The part I can not understand here is that adding the query string will make the page reloading work flawlessly (loads R - G - B - R - G - ...) - this is the uncommented line:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#?%d", #"http://www.cacheddemo.co.uk/index.html", arc4random()]]]];
Once we get rid of the query string the NSURLCache stops being hit other than the first request so it just stays on the R page:
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.cacheddemo.co.uk/index.html"]]];
The fact that the query string causes the NSURLCache to act as it is supposed to indicates to me that the browser cache is interfering in some way. In my head I figure the caching levels work as so:
Check browser cache
If nothing returned so far - check NSURLCache
If nothing returned so far - check proxy server cache
Finally attempt to load the remote resource
How can we disable the browser cache entirely so we can completely control caching behaviour for UIWebView. Unfortunately I do not see an option to set the Cache-Control header in NSURLCache - I already tried returning a NSHTTPURLResponse with the headers set but this seems to be ignored.
I am not sure I understand correctly, but the cache-control has to be set on server side, with something like no-cache, expires and so on, and not on iOS side.
Second, by modifying the query string i.e. www.mysite.com/page?id=whatever..., iOS and any browser think the request is not the same, if you have opened the cache itself with some db editor, you should have seen one request, which is one database entry, for each changed query.
This trick of adding a random query string is quite useful for avoiding the browser to cache javascript file.
I hope I understand correctly your question.
The browser does appear to have its own "cache" above the networking layer's cache (i.e. NSURLCache).
Not sure if setting the cache control headers will solve it but if you want to try that you can do so in your code for cachedResponseForRequest. Where you create an NSURLResponse object you can use initWithURL:statusCode:HTTPVersion:headerFields: to set the header fields (including the cache control headers). In your cache control header you can try using both no-store and no-cache (e.g. http://blog.55minutes.com/2011/10/how-to-defeat-the-browser-back-button-cache/ ). If you try this please let us know if it does or does not work.
However, in practice I think the most pragmatic and maintainable solution will be to the use a number on the URL (i.e. cache busting). BTW instead of a random number you can use a simple increment that gets reset whenever the webview is allocated.

Displaying remote images in an UIImageview downloading progressively or like lazyloading [duplicate]

I have been trying to display large image from server, but I have to display it progressively.
I used subclass of UIView and in that I have taken UIImage object, in which I used NSURLConnection and its delegate methods, I also used
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
in which I am appending data and converting it to UIImage object, and drawing rect using the drawInRect: method of UIImage.
Everything is working fine, but the problem is, when image is being drawn on context, I cannot click anywhere else on screen until entire image is being drawn on to screen.
Is there any good solution, where I can click anywhere else even if image is being drawn on screen?
Any help will be appreciable.
Edit:
Is there any efficient way of drawing image blurry progressively in didReceiveData? so drawInRect does not take too much time to draw. Or If anyone has custom drawRect method which efficiently displays image progressively as data received in didReceiveData.
I have used NYXImagesKit for something similar, downloading images while not blocking the main thread and showing the image progressively. Ive written a really quick and dirty example to illustrate the basic workings. I load the image in a UITableview to show that it doesn't block the User Interface(Main Thread). You can scroll the tableview while the image is loading. Don't forget to add the correct Frameworks, there are a few. Heres the link to the project on Github:
https://github.com/HubertK/ProgressiveImageDownload
It's really easy to use,create a NYXProgressiveImageView object, set the URL and it will do all the work for you when you call:
loadImageAtURL:
It's a subclass of UIImageView, Works like magic! Here's a link to the developers site:
http://www.cocoaintheshell.com/2012/01/nyximageskit-class-nyxprogressiveimageview/
I suggest pulling the image data in an asynchronous manner and then applying a correction in order to obtain a valid conversion from partially downloaded NSData to an UIImage:
NSURLRequest *theRequest = [NSURLRequest requestWithURL:
[NSURL URLWithString: imageRequestString]
cachePolicy: NSURLRequestReloadIgnoringCacheData
timeoutInterval: 60.0];
NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest: theRequest
delegate: self];
if (theConnection)
receivedData = [[NSMutableData data] retain];
.......
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[receivedData appendData: data];
NSInvocationOperation *operation =
[[NSInvocationOperation alloc] initWithTarget: self
selector: #selector(loadPartialImage)
object: nil];
[[[NSOperationQueue alloc] init] autorelease] addOperation: operation];
[operation release];
}
- (void)loadPartialImage {
// This is where you would call the function that would "stitch up" your partial
// data and make it appropriate for use in UIImage's imageWithData
NSData *validPartialData =
[self validImageRepresentationFromPartialImageData: receivedData];
UIImage *partialImage = [UIImage imageWithData: validPartialData];
[imageView performSelectorOnMainThread: #selector(setImage:)
withObject: partialImage
waitUntilDone: NO];
}
+ (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[connection release];
UIImage *fullImage = [UIImage imageWithData: receivedData];
imageView.image = fullImage;
}
Note that I did not provide the code for validImageRepresentationFromPartialImageData, as, at the moment, I have no clear, specific idea, on how to implement such a correction, or if the [UIImage imageWithData:] wouldn't actually accept partial data as input by default. As you can see, the coercion and UIImage creation would happen on a different thread, while the main thread would only display the updates as they come.
If you are receiving too frequent updates and they are still blocking the interface, you could:
a. Make the image requests on a different thread as well.
b. Reduce the frequency of the UIImageView's updates, by only calling setImage once in 10 or 100 updates, according to the zise of your image.
I usually use a really simple GCD pattern for async image loading:
Create a GCD queue in which you load the image data form your web server
Set the image data in your main queue
Example:
dispatch_queue_t image_queue = dispatch_queue_create("com.company.app.imageQueue", NULL);
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(image_queue, ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]];
dispatch_async(main_queue, ^{
[imageView setImage:[UIImage imageWithData:imageData]];
});
});
Probably didReceiveData is called too often! Just use a NSTimerand update the image regularly in 1-2second steps. That should work more efficiently.
Also you can use performSelectorInBackgroundto convert your NSData to an UIImage;
And then call performSelectorOnMainThreadto set the image into the UIImage View. So the converting stuff won't block the main thread.
Have you considered chopping up your images into smaller chunks on the server, then redrawing whenever a complete chunk has been received? This would give you control over the "progressiveness" of the load and the frequency of redraws by changing the chunk size. Not sure this is the kind of progressive load you're after, though.
If you have control of the server, split the image into tiles and also create a low res image. Display the low res version first in the lowest layer, and load the tiles on top drawing them as they load?
You can create a subclass of UIImageView with the URL of the image and a startDownload method.
It's a basic sample it must be improved.
#property (nonatomic, strong) NSURL *imageURL;
- (void)startDownload;
#implementation ImgeViewSubClass
{
NSURLConnection *connection;
NSMutableData *imageData;
}
The start download method:
- (void)startDownload
{
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
imageData = [NSMutableData data];
}
Delegate method from NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
#synchronized(imageData)
{
[imageData appendData:data];
}
// this part must be improved using CGImage instead of UIImage because we are not on main thread
UIImage *image = [UIImage imageWithData:imageData];
if (image) {
[self performSelectorOnMainThread:#selector(setImage:) withObject:image waitUntilDone:NO];
}
});
}
The Answer is in ImageIO.framework , its very simple actually
first you create a CGImageSourceRef mySource ,instantiate it using CGImageSourceCreateIncremental() .
setup and start an NSURLConnection with the image Url.
in connection:didReceiveData: , append the received data to your placeholder data , and update the image source by calling
CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, NO);
then load the partially loaded part of the image to your UIImageView
self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];
in connectionDidFinishLoading: finalise by calling
CGImageSourceUpdateData(imageSource, (CFDataRef)imageData, YES);
self.image = [UIImage imageWithCGImage:CGImageSourceCreateImageAtIndex(imageSource, 0, nil)];
CFRelease(imageSource);
imageData = nil;
here is a sample code i wrote :
https://github.com/mohammedDehairy/MDIncrementalImageView
Why don't you use ASIHTTPRequest request:
#import "ASIHTTPRequest.h"
This will help to load/draw in background, can perform other task too.
Try this one:
#import "ASIHTTPRequest.h"
[self performSelectorInBackground:#selector(DownLoadImageInBackground:)
withObject:YOUR IMAGE ARRAY];
-(void) DownLoadImageInBackground:(NSArray *)imgUrlArr1
{
NSURL * url = [Image URL];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
-(void)requestFailed:(ASIHTTPRequest *)request
{
NSLog(#"URL Fail : %#",request.url);
NSError *error = [request error];
// you can give here alert too..
}
-(void)requestFinished:(ASIHTTPRequest *)request
{
/////////// Drawing Code Here////////////////////
NSData *responseData = [request responseData];
UIImage *imgInBackground = [[UIImage alloc] initWithData:responseData];
[imageView setImage: imgInBackground];
}
I am not sure how the other parts of your code(reg this module) is implemented but give the following a try,
Try to use this selector with the run loop mode set to NSDefaultRunLoopMode
[self performSelectorOnMainThread:#selector(processImage:)
withObject:objParameters
waitUntillDone:NO
modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]
This execution will free up your UI interactions, let me know if it helped please.
For more info : APPLE DOCS
//JImage.h
#import <Foundation/Foundation.h>
#interface JImage : UIImageView {
NSURLConnection *connection;
NSMutableData* data;
UIActivityIndicatorView *ai;
}
-(void)initWithImageAtURL:(NSURL*)url;
#property (nonatomic, retain) NSURLConnection *connection;
#property (nonatomic, retain) NSMutableData* data;
#property (nonatomic, retain) UIActivityIndicatorView *ai;
#end
//JImage.m
#import "JImage.h"
#implementation JImage
#synthesize ai,connection, data;
-(void)initWithImageAtURL:(NSURL*)url {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[self setContentMode:UIViewContentModeScaleToFill];
if (!ai){
[self setAi:[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]];
[ai startAnimating];
[ai setFrame:CGRectMake(27.5, 27.5, 20, 20)];
[ai setColor:[UIColor blackColor]];
[self addSubview:ai];
}
NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData {
if (data==nil)
data = [[NSMutableData alloc] initWithCapacity:5000];
[data appendData:incrementalData];
NSNumber *resourceLength = [NSNumber numberWithUnsignedInteger:[data length]];
NSLog(#"resourceData length: %d", [resourceLength intValue]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(#"Connection error...");
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[ai removeFromSuperview];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)theConnection
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self setImage:[UIImage imageWithData: data]];
[ai removeFromSuperview];
}
#end
//Include the definition in your class where you want to use the image
-(UIImageView*)downloadImage:(NSURL*)url:(CGRect)frame {
JImage *photoImage=[[JImage alloc] init];
photoImage.backgroundColor = [UIColor clearColor];
[photoImage setFrame:frame];
[photoImage setContentMode:UIViewContentModeScaleToFill];
[photoImage initWithImageAtURL:url];
return photoImage;
}
//call the function
UIImageView *imagV=[self downloadImage:url :rect];
//you can call the downloadImage function in looping statement and subview the returned imageview.
//it will help you in lazy loading of images.
//Hope this will help

Using AVURLAsset on a custom NSURLProtocol

I have written a custom NSURLProtocol (called "memory:") that allows me to fetch stored NSData items from a NSDictionary based on a name. For example, this code registers the NSURLProtocol class and adds some data:
[VPMemoryURLProtocol register];
[VPMemoryURLProtocol addData:data withName:#"video"];
This allows me to refer to the NSData via a url like "memory://video".
Below is my custom NSURLProtocol implementation:
NSMutableDictionary* gMemoryMap = nil;
#implementation VPMemoryURLProtocol
{
}
+ (void)register
{
static BOOL inited = NO;
if (!inited)
{
[NSURLProtocol registerClass:[VPMemoryURLProtocol class]];
inited = YES;
}
}
+ (void)addData:(NSData *)data withName:(NSString *)name
{
if (!gMemoryMap)
{
gMemoryMap = [NSMutableDictionary new];
}
gMemoryMap[name] = data;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSLog(#"URL: %#, Scheme: %#",
[[request URL] absoluteString],
[[request URL] scheme]);
NSString* theScheme = [[request URL] scheme];
return [theScheme caseInsensitiveCompare:#"memory"] == NSOrderedSame;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
NSString* name = [[self.request URL] path];
NSData* data = gMemoryMap[name];
NSURLResponse* response = [[NSURLResponse alloc] initWithURL:[self.request URL]
MIMEType:#"video/mp4"
expectedContentLength:-1
textEncodingName:nil];
id<NSURLProtocolClient> client = [self client];
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
- (void)stopLoading
{
}
I am not sure whether this code works or not but that is not what I have a problem with. Despite registering the custom protocol, canInitWithRequest: is never called when I try to use the URL in this code:
NSURL* url = [NSURL URLWithString:#"memory://video"];
AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:url options:nil];
AVAssetImageGenerator* imageGen = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
CMTime time = CMTimeMakeWithSeconds(0, 600);
NSError* error;
CMTime actualTime;
CGImageRef image = [imageGen copyCGImageAtTime:time
actualTime:&actualTime
error:&error];
UIImage* uiImage = [UIImage imageWithCGImage:image];
CGImageRelease(image);
image is always nil if I use "memory://video" but works fine if I use "file:///...". What am I missing? Why isn't canInitWithRequest not being called? Does AVFoundation only support specific URL protocols and not custom ones?
Thanks
Certainly the underpinnings used to only support particular URL schemes— as an eBook developer I've seen this happen for any media type loaded through a URL such as epub:// or zip://. In those cases, on iOS 5.x and earlier, tracing through the relevant code would wind up in a QuickTime method which compared the URL scheme against a small number of supported ones: file, http, https, ftp and whatever it is that iTunes uses-- I forget what it's called.
In iOS 6+ there is a new API in AVFoundation, however, which is designed to help here. While I've not used it personally, this is how it should work:
NSURL* url = [NSURL URLWithString:#"memory://video"];
AVURLAsset* asset = [[AVURLAsset alloc] initWithURL:url options:nil];
////////////////////////////////////////////////////////////////
// NEW CODE START
AVAssetResourceLoader* loader = [asset resourceLoader];
id<AVAssetResourceLoaderDelegate> delegate = [SomeClass newInstanceWithNSURLProtocolClass: [VPMemoryURLProtocol class]];
[loader setDelegate: delegate queue: some_dispatch_queue];
// NEW CODE END
////////////////////////////////////////////////////////////////
AVAssetImageGenerator* imageGen = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
CMTime time = CMTimeMakeWithSeconds(0, 600);
With this in place, you need only implement the AVAssetResourceLoader protocol somewhere, which is very simple as it contains only one method. Since you already have an NSURLProtocol implementation, all your real work is done and you can simply hand off the real work to the Cocoa loading system or your protocol class directly.
Again, I'll point out that I've yet to actually make use of this, so the above is entirely theoretical.

Resources