I am caching locally some of the larger files required for a UIWebView and have a subclass of NSURLCache with a custom implementation to help serve these files.
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
I am using this to hijack the requests and return a locally stored copy of the files (mainly t
The body of cachedResponseForRequest:request (without the boiler plate) is essentially:
// logic to figure out what the local file is, load it into a NSData object (f)
NSURLResponse *r = [[NSHTTPURLResponse alloc] initWithURL:request.URL MIMEType:mimetype expectedContentLength:[f length] textEncodingName:nil];
NSCachedURLResponse *cr = [[NSCachedURLResponse alloc] initWithResponse:r data:f] ;
[super storeCachedResponse:cr forRequest:request];
return cr;
This works correctly for all of the cached content apart from a single svg image. When the svg image is attempted to load it will proceed through cachedResponseForRequest:request, build a NSCachedURLResponse object and return it.
However the next thing the application does is download the file from the remote server and then any subsequent request are served from the cache. We dont want to download the file remotely as its relatively large and impacts performance.
Does the NSURLCache deal with .svg files differently to other filetypes?
The maximum size of files that will be handled by the NSUrlCache can be influenced by the parameters when you initialize the cache with:
initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)diskPath
The maximum file size is related to the memoryCapacity. You should set that to at least 10 (why?) times the maximum file size that you want to handle.
I have tried this with files up to 15MB using https://github.com/evermeer/EVURLCache
Related
Currently I'm using the following code to upload files to the server
NSURLRequest *urlRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:#"POST" URLString:[[entity uploadUrl]absoluteString] parameters:entity.params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// Get file url
[UploadModel getAassetUrl:entity.asset resultHandler:^(NSURL *fileUrl) {
NSError *fileappenderror;
// Append
[formData appendPartWithFileURL:fileUrl name:#"data" error:&fileappenderror];
if (fileappenderror) {
[Sys MyLog: [fileappenderror localizedDescription] ];
}
}];
} error:&urlRequestError];
/*getAassetUrl */
+(void)getAassetUrl: (PHAsset*)mPhasset resultHandler:(void(^)(NSURL *imageUrl))dataResponse{
PHImageRequestOptions * requestOption = [[PHImageRequestOptions alloc] init];
requestOption.synchronous = YES;
requestOption.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
[[PHImageManager defaultManager] requestImageDataForAsset:mPhasset options:requestOption resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
dataResponse([info objectForKey:#"PHImageFileURLKey"]);
}];
}
This approach works on a simulator, but fails on a real device: empty files are uploaded to the server most likely due to failure to read from the local storage.
Log shows the notice
Notice: Sandbox: MyApp(213) deny file-read-data
/private/var/mobile/Media/DCIM/101APPLE/IMG_1570.PNG
I believe this note means that app can't access the file by specified path.
Also I've tried an alternative approach uploading file by appending with NSData which is returned from request PHAsset data. but this approach is unusable in case of large media files. since the entire file is loaded into the memory.
Any thoughts?
You shouldn't use requestImageDataForAsset(_:options:resultHandler:) for large files. Reason being you don't want to load the entire media file into memory, you will quickly run out of memory and the app will crash. This typically means you shouldn't use it for large images or pretty much any video.
In my experience, attempting to upload directly from a PHAsset resource url will fail. Apple doesn't seem to grant us the permissions required to upload direct from PHAsset source files. See forum post here. This is a pain because it forces us to use a ton of extra disk space if we want to upload a video.
In order to get a local file url for a video file that you intend to upload, you'll want to use either:
requestExportSessionForVideo(_:options:exportPreset:resultHandler:)
or
requestAVAssetForVideo(_:options:resultHandler:)
You will use these methods to export a copy of the video file to a location on disk that you control. And upload from that file. Bonus feature: both of these methods will download the file from iCloud if necessary.
Check out the VimeoUpload library for details on all things related to video upload. Disclaimer: I'm one of the authors of the library.
Even if you're not uploading to Vimeo servers, you can use the PHAssetExportSessionOperation and ExportOperation classes included in VimeoUpload to do exactly what you're looking to do. See the repo README for details on obtaining a file url for a PHAsset. It also includes tools for obtaining a file url for an ALAsset.
If you're not interested in using PHAssetExportSessionOperation or ExportOperation, check out their implementations for details on how to use the Apple classes under the hood.
The NSData object returned by requestImageDataForAsset is memory mapped - so the entire file is not loaded into memory. So this method will for without any issues for images.
For videos you should use the appropriate methods requestExportSessionForVideo or requestAVAssetForVideo
If you can limit your deployment target to iOS 9, you should also take a look at the methods of PHAssetResourceManager
I am trying to download some files.
For each cell, I check if the file exists and is already stored in a directory, if not I download it.
But I also download files for the next couple of cells.
What happens, if I am trying to download a file for an invisible cell ahead of time, but then the user scrolls to the cell and the file is not fully downloaded yet.
Is there anyway to prevent it from downloading twice?
Is there anyway to know that the file is already getting downloaded?
I am using AFNetworking.
What you're trying to do has a name and it's caching.
AFNetworking already implements it as per the official FAQs
Does AFNetworking have any caching mechanisms built-in?
AFNetworking takes advantage of the caching functionality already provided by NSURLCache and any of its subclasses. So long as your NSURLRequest objects have the correct cache policy, and your server response contains a valid Cache-Control header, responses will be automatically cached for subsequent requests.
Set up the cache as follows and you'll be ok
- (void)setupCache {
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:1024*1024*4 // 1MB mem cache
diskCapacity:1024*1024*5 // 5MB disk cache
diskPath:nil];
[NSURLCache setSharedURLCache:urlCache];
}
Then just perform requests normally and if the resource is in cache you'll get a cache hit and you won't be downloading it twice.
Note that if you have to support anything below iOS5, you have to use an alternative URL cache like SDURLCache and set it up like follows
- (void)setupCache {
SDURLCache *urlCache = [[SDURLCache alloc] initWithMemoryCapacity:1024*1024 // 1MB mem cache
diskCapacity:1024*1024*5 // 5MB disk cache
diskPath:[SDURLCache defaultCachePath]];
[NSURLCache setSharedURLCache:urlCache];
}
The reason is well explained in this article, but it can be summarized by the following quote:
Before iOS5, NSURLCache just saved requests to memory, even if the documentation said otherwise - the diskCapacity property was silently ignored
Does anyone by coincidence know how I can empty the cache memory of the iOS app that I am developing, in the moment it goes to the background (applicationDidEnterBackground)? I have investigated about NSCache but I am still not able to understand how could I retrieve the cache to basically remove/free it?
Is this what you're talking about?
[[NSURLCache sharedURLCache] removeAllCachedResponses];
You can also modify the cache behavior of your requests to selectively cache responses. If you're using AFNetworking by chance, you can use setCacheResponseBlock. E.g. in one project I set it to return nil for all large video and audio files. But allow it to cache smaller image files.
[streamingOperation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
return nil; // Ensures we are not unecessarily caching asset data to Cache.db
}];
I subclassed NSURLCache and overriden – cachedResponseForRequest: and – storeCachedResponse:forRequest: . I am storing all the files am getting in the application's documents directory.
Now, the problem is, the .html file when opened doesn't load all the resources (i.e, images,js and css). I didn't know wat the problem was, hence I did a "save page as" on the same web page and compared both the .html files (the one i saved from the browser and the one i cached.).
I found that in the html file saved from the browser, all the images and other files source paths have been replaced with the local paths..
<div><img src="./CNN.com International - Breaking, World, Business, Sports, Entertainment and Video News_files/footer_cnn_logo.png" width="23" height="11" alt="" border="0" class="cnn_ie6png">
But the html file that i cached, still has the url of the website..
<div><img src="http://i.cdn.turner.com/cnn/.e/img/3.0/global/footer/pngs/footer_cnn_logo.png" width="23" height="11" alt="" border="0" class="cnn_ie6png"/>
So how do I change it..?or what's the right way to save a web page along with all the resources..??
AM saving all the files am getting in storeCachedResponse:forRequest: method as it is. So do I have to do some modifications to this .html file before saving, so that it would refer all the resource path in the local folder properly ??
FYI, I do not want to use ASIWebPageRequest (Download and cache entire web page on ios)
//////////// Edit ////////////
So as Edwin Vermeer has answered, am able to get the right local file from my cache and return the response. but looks like, the UIWebView doesn't understand that data..!
This is how am returning the response in cachedResponseForRequest: method
if (shouldFetchFromCache) {
NSData* content = [NSData dataWithContentsOfFile:storagePath];
NSDictionary *headers = #{#"Access-Control-Allow-Origin" : #"*", #"Access-Control-Allow-Headers" : #"Content-Type"};
NSHTTPURLResponse *urlresponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:#"1.1" headerFields:headers];
cachedresponse = [[NSCachedURLResponse alloc] initWithResponse:urlresponse data:content];
}
return cachedresponse;!
This is how the web page loads on the webView..
Screenshot of the app
Is there some different way to return the response...? by the way, neither content or the response are nil.
an example of the request url is
Request : http://z.cdn.turner.com/cnn/tmpl_asset/static/intl_homepage/1021/js/intlhplib-min.js
and the file path where its stored is :
file path: /Users/akshay/Library/Application Support/iPhone Simulator/5.1/Applications/86ED671C-AE45-449A-A2E1-D08281AB54CF/Documents/CVWebCache/cnn/tmpl_asset/static/intl_homepage/1021/js/intlhplib-min.js
If your custom NSUrlCache is still active, then the requests for the resources should go past it. You could there test for the URL and return the correct file from your documents folder.
You could then also open the original URL instead of the file from the documents folder. Again your NSUrlCache will return the file from your document folder.
You do not have to create a NSHTTPUrlResponse. A NSURLResponse is good enough.
if (shouldFetchFromCache) {
NSData* content = [NSData dataWithContentsOfFile:storagePath];
NSURLResponse* response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:#"cache" expectedContentLength:[content length] textEncodingName:nil] ;
cachedresponse = [[NSCachedURLResponse alloc] initWithResponse:response data:content];
}
}
return cachedresponse;
I use a UIWebView to load a local html, and there is a PNG file inside the html created by Objc.
After the PNG file has been modified, I reload the html in UIWebView, but the image doesn't change. However, if I quit the app and reopen it, the image file will be changed to the new one.
I have checked the PNG file in Documents with Finder, so I'm sure it has been modified, but the old one is still in UIWebView.
So, as I think that it's a UIWebView cache problem, I've tried:
[[NSURLCache sharedURLCache] removeAllCachedResponses];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:url isDirectory:NO ] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:1]]; or NSURLRequestReloadIgnoringCacheData
None of them works, and I can't change the filename, because the PNG file is used in a lot of places (other html and objc code).
I've tried this too:
some.png?r=randomNumber
but it can't be showed.
How do I clear the UIWebView cache when using a local image file inside a local html?
Other than renaming every file on each access, I've only seen one thing work for this and that is modifying the HTML with javascript to add a timestamp onto the image url so it tricks the webview into thinking it's a different file. Images (usually) load the same no matter what you put after the ? in their url. I think this would be easier than renaming every file each time you load the web view. Something like this (using jQuery):
<img src="myimg.jpg" />
<script>
$(function() {
$('img').each(function(i, el) {
$(el).attr('src', $(el).attr('src')+'?pizza='+(new Date()).getTime());
});
});
</script>
I guess this is assuming that this javascript loads and runs before the images are loaded, but in theory this should work.
For a little background, I've made a page in a webview that used RequireJS to asynchronously load quite a few files. I had the EXACT same problem that this question is talking about except that I was loading javascript files instead of images. The key to fixing this issue was adding a timestamp to every path of javascript file and thus tricking the webview (ex me.js?ts=236136236 and whatever.js?ts=3153524623). I found this to work great.
One other thing I needed to do was add a timestamp to the path of my HTML file when loading it into the webview like this:
[NSURL URLWithString:[[NSString stringWithFormat:#"%#/index.html?pizza=%f", webDirectoryPath, [[NSDate date] timeIntervalSince1970]]
I now can modify all the local files and each time the webview appears the changes come through.
You can try this, in your AppDelegate.m
+(void)initialize {
[[NSURLCache sharedURLCache] setDiskCapacity:0];
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
}
If your html didn't change and the only change was in image you should use UIWebView's reload message instead of loading request again.
Something like this:
- (void)reloadWebView:(id)sender
{
if (self.loaded) {
[self.webView reload];
} else {
NSString *html = [[NSBundle mainBundle] pathForResource:#"web" ofType:#"html"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:html]];
[self.webView loadRequest:request];
self.loaded = YES;
}
}
You don't even need any manipulations with cache.