iOS Downloading image in background and updating the UI - ios

I am trying update an imageview with new image for every 1 sec by downloading the image from the server. The downloading happens in background thread. Below shown is the code
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:#selector(timerFired:)
userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-(void)timerFired:(id)sender
{
NSURLRequest *request=[[NSURLRequest alloc]initWithURL:[NSURL
URLWithString:#"http://192.168.4.116:8080/RMC/ImageWithCommentData.jpg"]];
NSOperationQueue *queueTmp = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queueTmp completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
{
[self performSelectorOnMainThread:#selector(processImageData:) withObject:data waitUntilDone:TRUE modes:nil];
}
else if ([data length] == 0 && error == nil)
{
}
else if (error != nil)
{
}
}];
}
-(void)processImageData:(NSData*)imageData
{
NSLog(#"data downloaded");
[self.hmiImageView setImage:[UIImage imageWithData:imageData]];
}
My image is getting downloaded. But ProcessImageData method is not called. Please help me to fix this issue.

The problem is you are calling NSURLConnection asynchronously :
[NSURLConnection sendAsynchronousRequest:request queue:queueTmp completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
so before you get any data the : if ([data length] > 0 && error == nil) get called .
so the data length remains 0 thats why your method is not called . for achieving your requirement you can do like :
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
NSData *imageData = //download image here
UIImage* image = [[UIImage alloc] initWithData:imageData];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
//set image here
}
});
}
});

Related

Asynchronously download images for UITableView using NSURLConnection

I have a TableView with customCells, when user press Start button on some cell the loading starts. There are many such cells, so I need to implement this downloading in parallel (asynchronously).
For image downloading and updating the cell in Table view I use next code:
#define myAsyncQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
I include this method into the async queue, that I supposed should enable parallel downloading of images.
- (void)didClickStartAtIndex:(NSInteger)cellIndex withData:
(CustomTableViewCell*)data
{
dispatch_async(myAsyncQueue, ^{
self.customCell = data;
self.selectedCell = cellIndex;
ObjectForTableCell* tmp =[self.dataDictionary objectForKey:self.names[cellIndex]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:tmp.imeageURL]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:60.0];
self.connectionManager = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
});
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.urlResponse = response;
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
NSDictionary *dict = httpResponse.allHeaderFields;
NSString *lengthString = [dict valueForKey:#"Content-Length"];
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
NSNumber *length = [formatter numberFromString:lengthString];
self.totalBytes = length.unsignedIntegerValue;
self.imageData = [[NSMutableData alloc] initWithCapacity:self.totalBytes];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.imageData appendData:data];
self.customCell.progressView.progress = ((100.0/self.urlResponse.expectedContentLength)*self.imageData.length)/100;
float per = ((100.0/self.urlResponse.expectedContentLength)*self.imageData.length);
self.customCell.realProgressStatus.text = [NSString stringWithFormat:#"%0.f%%", per];
}
I tried to set this block to queue - main queue - cause its the place where image is already downloaded,
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
dispatch_async(dispatch_get_main_queue(), ^{
self.customCell.realProgressStatus.text = #"Downloaded";
UIImage *img = [UIImage imageWithData:self.imageData];
self.customCell.image.image = img;
self.customCell.tag = self.selectedCell;
});
[self.savedImages setObject:img forKey:self.customCell.nameOfImage.text];
NSNumber *myNum = [NSNumber numberWithInteger:self.selectedCell];
[self.tagsOfCells addObject:myNum];
}
Without all queues(when I comment it)all works properly - but just 1 downloading at a ones.
But when I tried to implement code with queues as a result it doesn't download anything. I understand that I did smh wrong but I can't define it.
Thanks a lot for any help in advance.
If your looking out for starting it form basics I guess you should start with NSURLSession as NSURLConnection most of implementation had been deprecated and won't be available after iOS 9. For complete reference URL Session Programming Guide and tutorial
Coming back to your question you should do something similar to this took it from tutorial
// 1
NSURLSessionDownloadTask *getImageTask =
[session downloadTaskWithURL:[NSURL URLWithString:imageUrl]
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error) {
// 2
UIImage *downloadedImage =
[UIImage imageWithData:
[NSData dataWithContentsOfURL:location]];
//3
dispatch_async(dispatch_get_main_queue(), ^{
// do stuff with image
_imageWithBlock.image = downloadedImage;
});
}];
// 4
[getImageTask resume];
But my personal recommendation is go for AFNetworking which is best for iOS networking and widely used/tested in iOS app world.
For image download using AFNetworking
[_imageView setImageWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://i.imgur.com/fVhhR.png"]]
placeholderImage:nil
success:^(NSURLRequest *request , NSHTTPURLResponse *response , UIImage *image ){
NSLog(#"Loaded successfully: %d", [response statusCode]);
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error){
NSLog(#"failed loading: %#", error);
}
];
EDIT : Async downloading using concurrency
// get main dispact queue
dispatch_queue_t queue = dispatch_get_main_queue();
// adding downloading task in queue using block
dispatch_async(queue, ^{
NSData* imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]];
UIImage* image = [[UIImage alloc] initWithData:imageData];
// after download compeletes geting main queue again as there can a possible crash if we assign directly
dispatch_async(dispatch_get_main_queue(), ^{
_imageWithBlock.image = image;
});
});
Use this sample code from Apple to solve your problem of lazy loading.

MWphotobrowser - Fetch Images from Json

I'm trying to figure out how to get MWphotobrowser to fetch photo, photo caption, photo thumbnail etc. from a json file an extermal server.
In viewDidLoad, I have this code:
- (void)viewDidLoad {
NSURL *url = [NSURL URLWithString:#"https://s3.amazonaws.com/mobile-makers-lib/superheroes.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[super viewDidLoad];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
NSLog(#"Back from the web");
}];
NSLog(#"Just made the web call");
}
In Case3 MWphotobrowser's Menu.m, I have the following code:
case 3: {
photo.caption = [self.result valueForKeyPath:#"name"];
NSArray * photoURLs = [self.result valueForKeyPath:#"avatar_url"];
NSString * imageURL = [photoURLs objectAtIndex:indexPath.row];
[photos addObject:[MWPhoto photoWithURL:[NSURL URLWithString:imageURL]]];
enableGrid = NO;
break;
}
Incase you missed it, the JSON file I'm using is https://s3.amazonaws.com/mobile-makers-lib/superheroes.json
Nothing I tweak seems to make it work, any ideas how to fix this?
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
// here make sure ur response is getting or not
if ([data length] >0 && connectionError == nil)
{
// DO YOUR WORK HERE
self.superheroes = [NSJSONSerialization JSONObjectWithData:data options: NSJSONReadingMutableContainers error: &connectionError];
NSLog(#"data downloaded.==%#", self.superheroes);
}
else if ([data length] == 0 && connectionError == nil)
{
NSLog(#"Nothing was downloaded.");
}
else if (connectionError != nil){
NSLog(#"Error = %#", connectionError);
}
}];
in here u r getting the response from server is NSArray -->NSMutableDictionary` so
Case scenario is
case 3: {
NSDictionary *superhero = [self.superheroes objectAtIndex:indexPath.row];
photo.caption = superhero[#"name"];
NSString * imageURL = superhero[#"avatar_url"];
// NSArray * photoURLs = superhero[#"avatar_url"];
[photos addObject:[MWPhoto photoWithURL:[NSURL URLWithString:imageURL]]];
enableGrid = NO;
break;
}
ur final out put is

Set UILabel text within code block

I have a code block for my NSURLConnection sendAsynchronousRequest, and when I try to set a UILabel's text, the text is never set, even though the values are there. Here's my code:
NSString *address = [addresses objectAtIndex:indexPath.row];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"http://myurl.com/%#", address]]];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ([data length] > 0 && error == nil)
{
NSString *dataOfData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(![dataOfData isEqualToString:#"ERROR: address invalid"]) {
[balanceLabel setText:[NSString stringWithFormat:#"Balance: %#", dataOfData]];
if(data) {
qrCodeButton.alpha = 1;
}
} else {
errorLabel.text = #"This address is invalid.";
}
}
else if ([data length] == 0 && error == nil)
{
NSLog(#"Nothing was downloaded.");
[balanceLabel setText:#"Server Error, Please Try Again"];
}
else if (error != nil){
NSLog(#"Error = %#", error);
}
}];
Why is the UILabel's text never set? Is there a limitation to code blocks? If so, how would I fix my problem? Cheers!
It is because an NSOperationQueue is not the main thread. What you're doing is illegal. And the sad thing is that there is no need for it! Just say:
[NSURLConnection sendAsynchronousRequest:urlRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ... and the rest exactly as you have it now
All fixed. Your request is asynchronous on a background thread, but when it comes back to you on the completion handler, you'll be on the main thread and able to talk to the interface etc.
Your code operates UI element should execute on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
if ([data length] > 0 && error == nil)
{
NSString *dataOfData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(![dataOfData isEqualToString:#"ERROR: address invalid"]) {
[balanceLabel setText:[NSString stringWithFormat:#"Balance: %#", dataOfData]];
if(data) {
qrCodeButton.alpha = 1;
}
} else {
errorLabel.text = #"This address is invalid.";
}
}
else if ([data length] == 0 && error == nil)
{
NSLog(#"Nothing was downloaded.");
[balanceLabel setText:#"Server Error, Please Try Again"];
}
else if (error != nil){
NSLog(#"Error = %#", error);
}
}) ;
Make sure errorLabel is not nil and the UILabel is visible (It is added in the view hierarchy and its frame is appropriate).

NSURLConnection taking a long time

This code loads a table view:
- (void)viewDidLoad
{
[super viewDidLoad];
//test data
NSURL *url =[[NSURL alloc] initWithString:urlString];
// NSLog(#"String to request: %#",url);
[ NSURLConnection
sendAsynchronousRequest:[[NSURLRequest alloc]initWithURL:url]
queue:[[NSOperationQueue alloc]init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] >0 && connectionError ==nil){
NSArray *arrTitle=[[NSArray alloc]init];
NSString *str=[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
arrTitle= [Helper doSplitChar:[Helper splitChar20] :str];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self fecthDataToItem:arrTitle];
[self.tableView reloadData];
NSLog(#"Load data success");
}else if (connectionError!=nil){
NSLog(#"Error: %#",connectionError);
}
}];
// arrTitle = [NSArray arrayWithObjects:#"ee",#"bb",#"dd", nil];
}
And it takes 10 - 15s to load. How can I make this faster?
.
Thanks Rob and rmaddy, problem is solve.
As rmaddy points out, you must do UI updates on the main queue. Failure to do so will, amongst other things, account for some of the problems you're experiencing.
The queue parameter of sendAsynchronousRequest indicates the queue upon which you want the completion block to run. So, you can simply specify [NSOperationQueue mainQueue]:
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] > 0 && connectionError == nil) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *arrTitle = [Helper doSplitChar:[Helper splitChar20] :str];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self fecthDataToItem:arrTitle];
[self.tableView reloadData];
} else if (connectionError!=nil) {
NSLog(#"Error: %#",connectionError);
}
}];
Or, if you where doing something slow or computationally expensive/slow within that block, go ahead and use your own background queue, but then dispatch the UI updates back to the main queue, e.g.:
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// do something computationally expensive here
// when ready to update the UI, dispatch that back to the main queue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// update your UI here
}];
}];
Either way, you should always do UI updates (and probably model updates, too, to keep that synchronized) on the main queue.

Does using blocks auto create new threads?

JUST started doing work with blocks... very confusing. I am using a block like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *myDictionary = [[mySingleton arrayPeopleAroundMe] objectAtIndex:indexPath.row];
NSMutableString *myString = [[NSMutableString alloc] initWithString:#"http://www.domain.com/4DACTION/PP_profileDetail/"];
[myString appendString:[myDictionary objectForKey:#"userID"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:[myString stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
completionHandler: ^( NSURLResponse *response,
NSData *data,
NSError *error)
{
[[mySingleton dictionaryUserDetail] removeAllObjects];
[[mySingleton arrayUserDetail] removeAllObjects];
if ([data length] > 0 && error == nil) // no error and received data back
{
NSError* error;
NSDictionary *myDic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
[mySingleton setDictionaryUserDetail:[myDic mutableCopy]];
NSArray *myArray = [myDic objectForKey:#"searchResults"];
[mySingleton setArrayUserDetail:[myArray mutableCopy]];
[self userDetailComplete];
} else if
([data length] == 0 && error == nil) // no error and did not receive data back
{
[self serverError];
} else if
(error != nil) // error
{
[self serverError];
}
}];
}
Once the connection is completed, this is called:
-(void)userDetailComplete {
ViewProfile *vpVC = [[ViewProfile alloc] init];
[vpVC setPassedInstructions:#"ViewDetail"];
[[self navigationController] pushViewController:vpVC animated:YES];
}
which caused this error to pop up:
"Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread."
The only way I got rid of the error was by changing userDetailComplete to this:
-(void)userDetailComplete {
dispatch_async(dispatch_get_main_queue(), ^{
ViewProfile *vpVC = [[ViewProfile alloc] init];
[vpVC setPassedInstructions:#"ViewDetail"];
[[self navigationController] pushViewController:vpVC animated:YES];
});
}
My question: is a new thread started automatically every time a block is used? Are there any other pitfalls I should aware of when using blocks?
Blocks do not create threads. They are closures; they just contain runnable code that can be run at some future point.
This is running on a background thread because that's what you asked it to do:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
...
You created a new queue and then asked NSURLConnection to call you back on that queue. If you want to be called back on the main thread, pass [NSOperationQueue mainQueue]. That's usually waht you want.

Resources