Reputation: 5301
First of all, this is not a duplicate question. I have read many questions on Stack Overflow, but they didn't help to solve my problem completely.
I am downloading images from a web service. As no one likes the UI to be stalled, I am using threads to download images separately.
NSURL *imageUrl = [NSURL URLWithString:storyImageURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
thumbnailData = [NSData dataWithContentsOfURL:imageUrl];
dispatch_async(dispatch_get_main_queue(), ^{
thumbnail = [UIImage imageWithData:thumbnailData];
});
});
If I use the code exactly as above, the UI won't halt until it gets the data from the web service BUT the images are not cached.
If I don't use threads, then the UI will stall, but the images are cached using the NSCoding methods (Archiving).
My question is: What can I do to use threads and cache the thumbnails at the same time? Please do not suggest any third party libraries.
UPDATE: After going through the code again and again, there could be two problems that i can think of:
1) Looks like NSKeyedArchiver and NSKeyedUnarchiver are being called before the thread is done downloading the images but thats just a guess. In a separate store file i am using NSKeyedArchiver and NSKeyedUnarchiver:
- (RSSChannel *)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *, NSError *))block
{
NSURL *url = [NSURL URLWithString:@"http://techcrunch.com/feed"];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
RSSChannel *channel = [[RSSChannel alloc] init];
TheConnection *connection = [[TheConnection alloc] initWithRequest:req];
//[connection setCompletionBlock:block];
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
cachePath = [cachePath stringByAppendingPathComponent:@"HAHAHA.archive"];
RSSChannel *cachedChannel = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];
if (!cachedChannel)
cachedChannel = [[RSSChannel alloc] init];
RSSChannel *channelCopy = [cachedChannel copy];
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
}
block(channelCopy, err);
}];
[connection setXmlRootObject:channel];
[connection start];
return cachedChannel;
}
2) Second problem that i can think of is that he UI is not refreshing after it tries to decode the thumbnail out of cache.
Upvotes: 3
Views: 4820
Reputation: 69027
1) Looks like NSKeyedArchiver and NSKeyedUnarchiver are being called before the thread is done downloading the images but thats just a guess. In a separate store file i am using NSKeyedArchiver and NSKeyedUnarchiver:
you are on the right track here.
you need a synchronisation mechanism between RSSChannel and the backgrounded tasks getting the data remotely, so that you call archiveRootObject
only after all the images have been downloaded.
one way to handle this is using a dispatch_group to handle all of your image downloads. you can then make your completion block wait on that dispatch group before executing archiveRootObject
. I wrote a gist for this some time ago, and I think it should help you as well: https://gist.github.com/sdesimone/4579906. If it does not, please report what exactly. (possibly you will need to fix some compilation errors).
another way of dealing with this would be managing a shared counter: you
increment the counter when the feed parsing starts and decrement it in the completion block:
RSSChannel *channelCopy = [cachedChannel copy];
INCREMENT_COUNTER
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) { if (!err) {
[channelCopy addItemsFromChannel:obj];
DECREMENT_COUNTER;
}
block(channelCopy, err);
}];
increment the counter each time you find an image to download, then decrement it when the image finished downloading; when the counter reaches zero, you know you can archive:
NSURL *imageUrl = [NSURL URLWithString:storyImageURL];
INCREMENT_COUNTER;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
thumbnailData = [NSData dataWithContentsOfURL:imageUrl];
dispatch_async(dispatch_get_main_queue(), ^{
thumbnail = [UIImage imageWithData:thumbnailData];
DECREMENT_COUNTER;
if (COUNTER_REACHED_ZERO)
CALL_ARCHIVE_METHOD_ON_CHANNEL OBJECT
});
});
this will require some refactoring: you need to store the channel as a property (so you can use it outside of the original method (see point 1).
I leave to you the decision as to how implement the shared counter; only take care of making its implementation thread-safe!
hope this helps.
Upvotes: 3
Reputation: 5384
try this
NSURL *imageUrl = [NSURL URLWithString:storyImageURL];
UIButton *btnThumbnail = [[UIButton alloc] initWithFrame:CGRectMake(0, 10, 180, 280)];
[self downloadingServerImageFromUrl:btnThumbnail AndUrl:imageUrl];
[btnThumbnail addTarget:self action:@selector(onSelectEPaper:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:viewPaperBg];
- (void)onSelectEPaper:(id)sender
{
}
-(void)downloadingServerImageFromUrl:(UIButton*)imgView AndUrl:(NSString*)strUrl
{
// strUrl = [strUrl encodeUrl];
// strUrl = [strUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString* theFileName = [NSString stringWithFormat:@"%@.jpg",[[strUrl lastPathComponent] stringByDeletingPathExtension]];
NSFileManager *fileManager =[NSFileManager defaultManager];
NSString *fileName = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"tmp/%@",theFileName]];
imgView.backgroundColor = [UIColor darkGrayColor];
UIActivityIndicatorView *actView = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[imgView addSubview:actView];
[actView startAnimating];
CGSize boundsSize = imgView.bounds.size;
CGRect frameToCenter = actView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
actView.frame = frameToCenter;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSData *dataFromFile = nil;
NSData *dataFromUrl = nil;
dataFromFile = [fileManager contentsAtPath:fileName];
// NSLog(@"%@",fileName);
if(dataFromFile==nil){
// NSLog(@"%@",strUrl);
NSString *url =[strUrl stringByReplacingOccurrencesOfString:@"\n" withString:@""];
url=[url stringByReplacingOccurrencesOfString:@"\t" withString:@""];
url=[url stringByReplacingOccurrencesOfString:@" " withString:@""];
url = [url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// dataFromUrl=[[[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]] autorelease];
// dataFromUrl=[[NSData dataWithContentsOfURL:[NSURL URLWithString:url]] autorelease];
NSError* error = nil;
// NSLog(@"%@", [NSURL URLWithString:url]);
dataFromUrl = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingUncached error:&error];
if (error) {
NSLog(@"%@", [error localizedDescription]);
} else {
// NSLog(@"Data has loaded successfully.");
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
if(dataFromFile!=nil){
// imgView.image = [UIImage imageWithData:dataFromFile];
[imgView setBackgroundImage:[UIImage imageWithData:dataFromFile] forState:UIControlStateNormal];
}else if(dataFromUrl!=nil){
// imgView.image = [UIImage imageWithData:dataFromUrl];
[imgView setBackgroundImage:[UIImage imageWithData:dataFromUrl] forState:UIControlStateNormal];
NSString *fileName = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"tmp/%@",theFileName]];
BOOL filecreationSuccess = [fileManager createFileAtPath:fileName contents:dataFromUrl attributes:nil];
if(filecreationSuccess == NO){
// NSLog(@"Failed to create the html file");
}
}else{
// imgView.image = [UIImage imageNamed:@"no_image.jpg"];
[imgView setBackgroundImage:[UIImage imageNamed:@"no_image.jpg"] forState:UIControlStateNormal];
}
[actView removeFromSuperview];
// [actView release];
// [imgView setBackgroundColor:[UIColor clearColor]];
});
});
}
Upvotes: 0
Reputation: 100602
Assuming you're targeting iOS 5+, the most natural solution would be to use NSURLCache
and NSURLConnection +sendAsynchronousRequest:queue:completionHandler:
as appropriate. Third party solutions usually simply ignore those methods either through ignorance or through a desire to support iOS 4 so your options are either effectively to farm out maintenance of this stuff to Apple, to trust a third party or to spend your own time on it.
E.g.
NSURLRequest *request =
[NSMutableURLRequest requestWithURL:imageURL];
// just use the shared cache unless you have special requirements
NSURLCache *cache = [NSURLCache sharedURLCache];
NSCachedURLResponse *response = [cache cachedResponseForRequest:request];
// we'll just lazily assume that if anything is in the
// cache then it will do
if(response)
{
[self proceedWithData:response.data];
}
else
{
// fetch the data
[NSURLConnection
sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue] // this dictates where the completion
// handler is called; it doesn't make
// the fetch block the main queue
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
// TODO: error handling here
[cache
storeCachedResponse:
[[NSCachedURLResponse alloc]
initWithResponse:response dat:data]
forRequest:request];
[self proceedWithData:data];
}];
}
NSURLCache
existed prior to iOS 5 but was a memory cache only. Since 5 it is also a disk cache.
Upvotes: 0
Reputation: 11436
Are you saving thumbnailData
as an instance variable? Before you dispatch check that to see if the instance variable is set, if so return that value. If not then run your dispatch block and save it as the instance variable.
Upvotes: 0