I started studying ObjectiveC not too long ago and I'm trying to write an app that has two UIImageViews and an array of URLs to load from web. After loading all the images from the URLs asynchronously, I would like to start animation on the UIImageViews that change pictures every 3 seconds. My only issue is that I don't want the animations until after all the images have been downloaded. the code I've got far is:
- (void)viewWillAppear:(BOOL)animated
{
self.imagesArray = [[NSMutableArray alloc] init];
[self initUrlArrayAndGetImages];
}
- (void)initUrlArrayAndGetImages
{
self.urlsArray = #[#"abc.com/1.png",
#"abc.com/2.png",
#"abc.com/3.png"];
for (int i = 0; i < [self.urlsArray count]; i++)
{
NSString *string = [self.urlsArray objectAtIndex:i];
NSURL *urlFromString = [NSURL URLWithString:string];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:urlFromString];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
}];
}
So where will I call the method that activates the animation from? Because if I call it at the completion of the block it adds an image at a time (very fast, true, but still refreshes the method all the time...). What is my best solution?
Thanks in advance
You could add an if() statement checking to see if the count of images in the imagesArray is equal to the count of URLs in the urlsArray. If so, then call the animation. This would look like:
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
if(self.imagesArray.count == self.urlsArray.count)
{
// All images are loaded successfully
// Activate animation
}
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
It's important to note that this will only work if all images were downloaded successfully. If one or more result in a connection error, it will not call the animation. To get around this, you could easily implement a dedicated count variable that is incremented when the completionHandler is called, whether or not there was a connection error. Then use this to compare to self.urlsArray.count.
In your completion handler you're adding the downloaded images to your self.imagesArray. Add code that compares imagesArray.count to urlsArray.count. If they are equal, all downloads have been completed and you can begin your animation:
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
if (self.imagesArray.count == self.urlsArray.count)
{
//All downloads are complete. Trigger the animation
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
}];
Related
In this project I use Foursquare API to get some venues and one photo from each venue. When I have those data, I'm reloading the TableView to display all the info.
First I collect the venues and secondly for each venue, I m taking a photo from foursquare API.
I m using the RestKit library for this project and I'm calling this method n times (one time for each venue). When it's finishing I want to display all those photos I have taken to my table view.
- (void)requestVenuePhoto:(Venue *)thisVenue{
//...
//...
[[RKObjectManager sharedManager] getObjectsAtPath:objectPath parameters:queryParams success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult)
{
[self.photos addObjectsFromArray:mappingResult.array];
//[self.tableView reloadData];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"What do you mean by 'there is no photos?': %#", error);
dispatch_group_leave(resolveVenuePhotos);
}];
}
The problem is that I can't use the dispatch_group_leave because is'not called one time only.
Is there any way to do this nicely?
Update, now I'm using a counter to solve the problem:
[[RKObjectManager sharedManager] getObjectsAtPath:objectPath parameters:queryParams success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult)
{
[self.photos addObjectsFromArray:mappingResult.array];
venuesPhotoCounter++;
if (venuesPhotoCounter == _venues.count)
{
[self.tableView reloadData];
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"What do you mean by 'there is no photos?': %#", error);
}];
Apple has uploaded a project called "LazyTableImages" available here
. So I wrapped it up and made some paradigms which should probably fit your use-case.
- (void)startDownload
{
NSURLRequest *request = [NSURLRequest requestWithURL:self.venue.imageURL];
// create an session data task to obtain and download the app icon
_sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// in case we want to know the response status code
NSInteger HTTPStatusCode = [(NSHTTPURLResponse *)response statusCode];
if (error != nil)
{
if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection)
{
// if you get error NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022),
// then your Info.plist has not been properly configured to match the target server.
//
abort();
}
}
[[NSOperationQueue mainQueue] addOperationWithBlock: ^{
// Set appIcon and clear temporary data/image
UIImage *image = [[UIImage alloc] initWithData:data];
if (HTTPStatusCode == 200) {
if (image.size.width != kAppIconSize || image.size.height != kAppIconSize)
{
CGSize itemSize = CGSizeMake(kAppIconSize, kAppIconSize);
UIGraphicsBeginImageContextWithOptions(itemSize, NO, 0.0f);
CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height);
[image drawInRect:imageRect];
self.venue.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
else
{
self.venue.image = image;
}
}
else {// If anything goes wrong we should use a placeholder image
self.venue.image = [UIImage imageNamed:#"Placeholder.png"];
}
// call our completion handler to tell our client that our icon is ready for display
if (self.completionHandler != nil)
{
self.completionHandler();
}
}];
}];
[self.sessionTask resume];
}
This is not a direct answer to your question, but I think it will resolve your issue.
You should not load all the pictures and only then reload the table. You should:
load the list of items, and in your completion handler, reload the table (make sure the reload happens on the main thread if you completion handler runs on a background thread).
then, in the tableView:cellForItemAtIndexPath: method of your tableview datasource, start loading the image for the requested item. Also make a note of the current indexPath (often just the row), for instance in the tag of the cell, or in a custom cell property. In the completion handler for that request, decode the image (in the background), then assign it to your cell (on the main thread), after having check that the cell is still for the same indexPath (i.e. the cell has not been reused).
Example using standard NSURLSession (based on a single section, and data being in an _items instance variable):
- tableView:(UITableView *)tableView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyItem *item = _items[indexPath.row];
MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:#"whatever"];
// configure the rest of the cell: labels, etc.
cell.tag = indexPath.row;
[[[NSURLSession sharedSession] dataTaskWithURL:item.url
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
// Remember this runs on a background thread
if (cell.tag == indexPath.row && !error && data)
{
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(),^
{
cell.myImageView.image = image;
});
}
}] resume];
}
In my app I am downloading image using blocks but it is freezing my UI. I have one network class which contains method to download image,
-(void)downloadImageWithCompletionHandler:^(NSData *aData, NSError *error)aBlock;
I am calling above method in my view controller to download image. So once the image is downloaded I am using NSData to show in image view. The network class method uses NSURLConnection methods to download the image.
[[NSURLConnection alloc] initWithRequest:theURLRequest delegate:self];
Once the data download is complete I am calling completion handler block of the view controller.
But I am not sure why my UI is freezing? Can anyone help me find where I am doing wrong?
Thanks in advance!
- (void) setThumbnailUrlString:(NSString *)urlString
{
NSString *url= [NSString stringWithFormat:#"%#",urlString];
//Set up Request:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]init];
[request setURL:[NSURL URLWithString:url]];
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
if ( queue == nil ){
queue = [[NSOperationQueue alloc] init];
}
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse * resp, NSData *data, NSError *error)
{
dispatch_async(dispatch_get_main_queue(),^
{
if ( error == nil && data )
{
UIImage *urlImage = [[UIImage alloc] initWithData:data];
_headImageView.image=urlImage;
_backgroundImageView.image=urlImage;
}
});
}];
}
You need to download the image in background thread to avoid freezing the UI thread.There is a simple demo to achieve this.
- (void)downloadImageWithCompletionHandler:(void(^)(NSData *aData, NSError *error))aBlock {
NSURLRequest *theURLRequest = nil; // assign your request here.
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[NSURLConnection sendAsynchronousRequest:theURLRequest queue:mainQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// UIThread.
aBlock(data,connectionError);
}];
}
how to call this method.
[self downloadImageWithCompletionHandler:^(NSData *aData, NSError *error) {
// get UIImage.
UIImage *image = [UIImage imageWithData:aData];
}];
I figured out the problem. Problem was not in the block or using NSUrlConnection method, it is working properly. Problem was, I was saving data in file once I download it. This operation was happening on main thread which was blocking the UI.
Basically I want a way to issue a NSURLRequest multiple times in a loop until a certain condition has been met. I am using a rest api but the rest api only allows up to a maximum of 1,000 results at a time. So if i have, lets say 1,500 total, i want to make a request to get the first 1,000 then i need to get the rest with another almost exact request , except the startAt: parameter is different(so i could go from 1001 - 1500. I want to set this up in a while loop(while i am done loading all the data) and am just reading about semaphores but its not working out like I expected it to. I don't know how many results I have until i make the first request. It could be 50, 1000, or 10,000.
here is the code:
while(!finishedLoadingAllData){
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if(error){
completionHandler(issuesWithProjectData, error);
}
else{
NSDictionary *issuesDictionary = [[NSDictionary alloc] initWithDictionary:[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]];
[issuesWithProjectData addObjectsFromArray:issuesDictionary[#"issues"]];
if(issuesWithProjectData.count == [issuesDictionary[#"total"] integerValue]){
completionHandler([issuesWithProjectData copy], error);
finishedLoadingAllData = YES;
}
else{
startAt = maxResults + 1;
maxResults = maxResults + 1000;
}
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
Basically I want to keep the while loop waiting until the completion block finished. Then and only then do i want the while loop to check if we have all of the data or not(and if not, make another request with the updated startAt value/maxResults value.
Right now it just hangs on dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
What am i doing wrong or what do i need to do? Maybe semaphores are the wrong solution. thanks.
Ok. The more I look, the more I don't think its a bad idea to have semaphores to solve this problem, since the other way would be to have a serial queue, etc. and this solution isn't all that more complicated.
The problem is, you are requesting the completion handler to be run on the main thread
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
and you are probably creating the NSURL request in the main thread. Hence while it waits for the semaphore to be released on the mainthread, the NSURL completion handler is waiting for the mainthread to be free of its current run loop. So create a new operation queue.
would it not be easier to do something like this instead:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //run on a background thread
while(!finishedLoadingAllData){
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
NSHTTPURLResponse *response = nil;
NSError *error = nil;
NSData *responseData = [NSURLConnection sendSynchronousRequest:myRequest returningResponse:&response error:&error]; //blocks until completed
if(response.statusCode == 200 && responseData != nil){ //handle response and set finishedLoadingAllData when you want
//do stuff with response
dispatch_sync(dispatch_get_main_queue(), ^{
//do stuff on the main thread that needs to be done
}
}
});
Please dont do that.. NSURLConnection sendAsynchronousRequest will be loading itself in loop for you, if your data is in chunk.. try this instead..
__block NSMutableData *fragmentData = [NSMutableData data];
[[NSOperationQueue mainQueue] cancelAllOperations];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
[fragmentData appendData:data];
if ([data length] == 0 && error == nil)
{
NSLog(#"No response from server");
}
else if (error != nil && error.code == NSURLErrorTimedOut)
{
NSLog(#"Request time out");
}
else if (error != nil)
{
NSLog(#"Unexpected error occur: %#", error.localizedDescription);
}
else if ([data length] > 0 && error == nil)
{
if ([fragmentData length] == [response expectedContentLength])
{
// finished loading all your data
}
}
}];
I've created two chunky json response from server handling method.. And one of them is this, so hope this will be useful to you as well.. Cheers!! ;)
I have a problem with my application.It freeze for several second when I tap the sidebar menu.
What happen when I tapped menu is I pass string that gonna be url for json data fetch in my mainviewcontroller.Then it freeze because I fetch the data and populating data in tableview.
However I really new to ios programming,I wonder how can I remove the freeze?.
thanks in advance
here is my code snippet for the mainviewcontroller:
Don't use dataWiyhContentsOfURL:, or at least not directly on the main thread. If you block the main thread then the whole app stops working (as you see).
You need to learn about background threads and callback blocks, and look at using NSURLSession to download your data and then process it.
Instead of using dataWithContentsOfURL (which will block the main thread and so the UI) you need to start an asynchronous connection. In the IF ELSE change the two requests to something like below. The completionHandler (Block) is executed when done, the data parsed, HUD removed and table Updated.
You can even (and in fact must) do this within your cellForRowAtIndexPath for each of the images, however, I would use SDWebImage as it has a cache and is very easy to use.
There are also other methods if this is not right for you such as NSURLSession.
Some other points;
I have also noted that the HUD is stopped on every iteration of the FOR and probably should be outside.
I also can not see how your data is being loaded so I added a [myTable reloadData];
I can not see that the "dictionary" object is needed as it can be added directly to the array (see code)
// If you have the status bar showing
// [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[HUD showUIBlockingIndicatorWithText:#"Please wait. . ."];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kategori]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
// [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (data != nil && error == nil)
{
//All Worked
id jsonObjects = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
for (NSDictionary *dataDict in jsonObjects)
{
NSString *title_data = [dataDict objectForKey:#"title"];
NSString *thumbnail_data = [dataDict objectForKey:#"thumb"];
NSString *author_data = [dataDict objectForKey:#"creator"];
NSString *link_data = [dataDict objectForKey:#"link"];
[myObject addObject:[[NSDictionary alloc]initWithObjectsAndKeys:
title_data, title,
thumbnail_data, thumbnail,
author_data,author,
link_data,link,
nil]];
}
[HUD hideUIBlockingIndicator];
[myTableView reloadData];
}
else
{
// There was an error
}
}];
For the images something like (this is not tested). I am not sure what format your images are in but you should be able to just add it, this may need tweeking;
cell.imageView.frame = CGRectMake(0, 0, 80, 70);
__block UIImageView *cellImage = cell.imageView;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[tmpDict objectForKey:thumbnail]]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (data != nil && error == nil)
{
//All Worked
cellImage.image = [[UIImage alloc]initWithData:data];
[cellImage layoutIfNeeded];
}
else
{
// There was an error
}
}];
You can start activity indicator and call fetch data method after few time...
- (void)viewDidLoad{
[activityIndicator startAnimating];
[self performSelector:#selector(fetchData) withObject:nil afterDelay:0.5];
}
- (void)fetchData{
Fetch your data over here
}
Or ideally you have to load data Asynchronous
For loading data Asynchronously check out the following link-
iphone-synchronous-and-asynchronous-json-parse
I Prefer MBProgressHUD.
Here is the link for 3rd Party API.
https://github.com/jdg/MBProgressHUD
Just copy these two files in your app.
MBProgressHUD.h
MBProgressHUD.m
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.