I know that there are a lot of similar questions to this one, but i didn't find any well-explained one yet.
I have a UITableView that gets its content (which is JSON) from a url, i'm using this method for fetching the JSON data:
-(void)getContents
{
NSString *contentStartString = [NSString stringWithFormat:#"%ld",(long)contentStart];
NSString *contentCountString = [NSString stringWithFormat:#"%ld",(long)contentCount];
NSString *contentsUrl = #"http://www.ana.fm/api/index.php?start=";
contentsUrl = [contentsUrl stringByAppendingString:contentStartString];
contentsUrl = [contentsUrl stringByAppendingString:#"&count="];
contentsUrl = [contentsUrl stringByAppendingString:contentCountString];
NSLog(#"%#",contentsUrl);
NSURL *URL = [NSURL URLWithString:contentsUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
//AFNetworking asynchronous url request
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]
initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
jsonContents = [responseObject objectForKey:#"contents"];
[self.tableView reloadData];
tableLoadMoreCapability = true;
} failure:nil];
[operation start];
}
This works perfectly, but then when i reach the end of the table which i can detect using this method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
if(tableLoadMoreCapability == true){
contentStart = contentStart + 20;
[self updateContentsTable];
}
}
}
As you see i want to execute this method updateContentsTable (when reaching the end of the table) which is this one:
- (void)updateContentsTable
{
tableLoadMoreCapability = false;
NSLog(#"load more rows");
NSString *contentStartString = [NSString stringWithFormat:#"%ld",(long)contentStart];
NSString *contentCountString = [NSString stringWithFormat:#"%ld",(long)contentCount];
NSString *contentsUrl = #"http://www.ana.fm/api/index.php?start=";
contentsUrl = [contentsUrl stringByAppendingString:contentStartString];
contentsUrl = [contentsUrl stringByAppendingString:#"&count="];
contentsUrl = [contentsUrl stringByAppendingString:contentCountString];
NSLog(#"%#",contentsUrl);
NSURL *URL = [NSURL URLWithString:contentsUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
//AFNetworking asynchronous url request
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]
initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
jsonContents = [responseObject objectForKey:#"contents"];
[self.tableView reloadData];
} failure:nil];
[operation start];
}
Now i'm using [self.tableView reloadData]; so the code works and the table reloaded with the new data, i don't want that, i need the new rows to be inserted below the old ones, i learned from other answers that i have to use [self.tableView insertRowsAtIndexPaths: .....] but i didn't understand how to do that.
Anyone can clarify how to do that ?
Thanks in advance.
I think you are doing more work than you need. Why not tell the table view that the number of rows you have is larger than the number you get with your first call? Then, as soon as the delegate is asked for a cell at a row beyond the ones you have actually loaded (indicating that the user has scrolled past the data you have available), you then get your next batch of data via JSON. Through the delegate methods, you have precise control over what data is shown in the visible cells. I would avoid the scrolling stuff completely.
The only care you need to take is when (and how) you reload the table: you want to avoid reload calling reload calling reload....
Think of the rows of the table as a window sliding over a (virtual) table of all your rows.
Related
I am trying to update my custom label using GCD however all I am getting is the text I initialized the label with:
-(void)drawCardInfo{
__block NSString *cardString;
PPLinearLayoutLabelItem *cardDetails = [[PPLinearLayoutLabelItem alloc] initWithText:#"CREDIT CARD INFO" font:[PPFonts regular18] maxWidth:[LayoutValues getMaxWidthClipped]];
[cardDetails setPaddingTop:20];
[cardDetails setPaddingBottom:10];
[self.topContainerContent addObject:cardDetails];
PPLinearLayoutLabelItem *cardInfo = [[PPLinearLayoutLabelItem alloc] initWithText:#"Data" font:[PPFonts bold16] maxWidth:[LayoutValues getMaxWidthClipped]];
[cardInfo setPaddingTop:0];
[self.topContainerContent addObject:cardInfo];
LinearLayoutHorizontalLine *line1 = [[LinearLayoutHorizontalLine alloc] initWithMaxWidth:[LayoutValues getMaxWidthClipped]];
[self.topContainerContent addObject:line1];
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSOperationQueue *networkQueue = [[NSOperationQueue alloc] init];
networkQueue.maxConcurrentOperationCount = 5;
NSURL *url = [NSURL URLWithString:#"https://xxxxxxxxxxxxxxxxxx"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]
initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
if([string isEqualToString:#""]){
} else {
NSMutableDictionary *dict=[NSJSONSerialization JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
NSString *cardType = [dict objectForKey:#"credit_card_type"];
NSString *cardFinalFour = [dict objectForKey:#"credit_card_last_4"];
cardString = [NSString stringWithFormat:#"%# **** %#",cardType, cardFinalFour];
NSLog(#"%#",cardString);
}
dispatch_async(dispatch_get_main_queue(), ^{ [cardInfo setText:cardString]; });
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s: AFHTTPRequestOperation error: %#", __FUNCTION__, error);
}];
[networkQueue addOperation:operation];
});
}
I am successfully logging out the value proving that the network call works fine on a separate thread. Still my label is not being updated. How can I mitigate this?
Looks like you are trying to update the text for the label in a background thread. Updating UI elements on anywhere other than the main thread is not such a good idea since you never know when a asynchronous job will finish.
You can use the following ways to go about it:
Get the main thread and then update the text label
dispatch_async(dispatch_get_main_queue(), ^{
//Code here to which needs to update the UI in the UI thread goes here
});
Use dispatch_sync
Use NSOperations with dependancies (This will be the longest)
I have an app which downloads a set of photos from a server. I am using an Asynchronous request because I don't want the UI to be blocked. However, I am finding that the request is very slow and takes ages to load.
I know you can set the queue type to [NSOperationQueue mainQueue] but that just puts the Asynchronous request back on the main thread which defeats the whole point of making the request Asynchronously in the first place.
Is there anyway to speed up the request or to tell iOS: "Run this request in the background, but do it ASAP, don't leave it till the end of the queue"???
Here is my code:
// Set up the photo request.
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:PHOTO_URL, pass_venue_ID, PHOTO_CLIENT_ID, PHOTO_CLIENT_SECRET]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Begin the asynchromous image loading.
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error == nil) {
// Convert the response data to JSON.
NSError *my_error = nil;
NSDictionary *feed = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&my_error];
// Check to see if any images exist
// for this particular place.
int images_check = [[NSString stringWithFormat:#"%#", [[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"count"]] intValue];
if (images_check > 0) {
// Download all the image link properties.
images_prefix = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"prefix"];
images_suffix = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"suffix"];
images_width = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"width"];
images_height = [[[[feed objectForKey:#"response"] valueForKey:#"photos"] valueForKey:#"items"] valueForKey:#"height"];
// Set the image number label.
number_label.text = [NSString stringWithFormat:#"1/%lu", (unsigned long)[images_prefix count]];
// Download up to 5 images.
images_downloaded = [[NSMutableArray alloc] init];
// Set the download limit.
loop_max = 0;
if ([images_prefix count] > 5) {
loop_max = 5;
}
else {
loop_max = [images_prefix count];
}
for (NSUInteger loop = 0; loop < loop_max; loop++) {
// Create the image URL.
NSString *image_URL = [NSString stringWithFormat:#"%#%#x%#%#", images_prefix[loop], images_width[loop], images_height[loop], images_suffix[loop]];
// Download the image file.
NSData *image_data = [NSData dataWithContentsOfURL:[NSURL URLWithString:image_URL]];
// Store the image data in the array.
[images_downloaded addObject:image_data];
}
// Load the first image.
[self load_image:image_num];
}
else if (images_check <= 0) {
// error...
}
}
else {
// error
}
}];
Thanks for your time, Dan.
i think your problem isnt the request running slow, its that you are updating UI elements not on the main thread, surround any UI updates (like setting the text on labels) with
dispatch_sync(dispatch_get_main_queue(), ^{
<#code#>
});
As Fonix said its not iOS that responding slow but dataWithContentsOfURL doesn't work in background thread. Apple's recommendation is that you should use NSURLConnection asynchronously with delegates
- didReceiveResponse
- didReceiveData
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:theURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:_mAuthenticationTimeoutInterval];
In these methods you can make use of chunks of data as well.
If you actually want these multiple downloads to be faster you should use parallel downloading using NSOperationQueue. You can refer enter link description here
I think a good solution could be using AFNetworking when combined with NSOperation, check this code I wrote to do more than one operation asynchronously
NSMutableArray *operations = [[NSMutableArray alloc] init];
for(NSObject *obj in caches) {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:url];
//...set up your mutable request options here
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
operation.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"application/json"];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSInteger statusCode = operation.response.statusCode;
if(statusCode==200) {
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"API Call error:%#", error.localizedDescription);
}];
[[requestManager operationQueue] addOperation:operation];
[operations addObject:operation];
if([operations count] >= MAX_API_CALL) break;
}
[AFHTTPRequestOperation batchOfRequestOperations:operations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
} completionBlock:^(NSArray *operations) {
NSError *error;
for (AFHTTPRequestOperation *op in operations) {
if (op.isCancelled){
}
if (op.responseObject){
// process your responce here
}
if (op.error){
error = op.error;
}
}
}];
I'm having a problem which is similar to others on SE, in that my UITableView controller loads the text label immediately, but only loads my thumbnail image when I scroll the view and move the item offscreen.
I tried adding [self.tableView reloadData] to the AFHTTPRequestOperation setCompletionBlockWithSuccess, which works with one drawback. It obviously runs too often.
Here is the method in which the problem occurs:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *fullPath;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TableViewCell" forIndexPath:indexPath];
Child *child = _children[indexPath.row];
if([child.data.thumbnail length] == 0) {
fullPath = #"reddit.png";
} else {
// Get the thumbnail
NSURL *url = [NSURL URLWithString:child.data.thumbnail];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[url lastPathComponent]];
[operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:fullPath append:NO]];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"bytesRead: %lu, totalBytesRead: %lld, totalBytesExpectedToRead: %lld", (unsigned long)bytesRead, totalBytesRead, totalBytesExpectedToRead);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// [self.tableView reloadData];
NSLog(#"RES: %#", [[[operation response] allHeaderFields] description]);
NSError *error;
if(error) {
NSLog(#"ERR: %#", [error description]);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERR1: %#", [error description]);
}];
[operation start];
}
cell.textLabel.text = child.data.title;
cell.imageView.image = [UIImage imageNamed:fullPath ];
return cell;
}
Instead of reloading the entire table view every time an image is loaded, you could just set that image directly on the cell inside you completion block.
HOWEVER, if you do that you need to check that the cell is still visible and that it is still on the same index path it was on when you started loading the view, otherwise you might be setting the image on a cell that has been reused and is now in a different position in the table view.
I'm new in iOS programming. So I've newbie question. I'm getting started with AFNetworking 2 and that is the task:
I've a request. Its response is the part of the second request. It means that I have to wait untill first request ends. They follow step-by-step. When I get the second response I parse it and save 20 URLs in format http://lalala-xx.jpg. After that I want to load images and put them into UICollectionView, and I want to do it not all in scope but in scheme "downloaded->straight to cell". I save URLs and images in singleton class and get access to them just like
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
cell.imageView.image = [[UserData sharedUser].images objectAtIndex:indexPath.row];
return cell;
}
The chain of methos looks like
- (void)method1
{
NSString *string = [NSString stringWithFormat:firstRequestString];
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
// getting needed part for second request
[self method2:(NSString *)part1];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[operation start];
}
Second method:
- (void)method2:(NSString *)part1
{
// lalala making secondRequestString
NSString *string = [NSString stringWithFormat:secondRequestString];
NSURL *url = [NSURL URLWithString:string];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSMutableArray *imageURLs = [[NSMutableArray alloc] init];
// getting needed URLs
[self loadAllImages:(NSMutableArray *)imageURLs];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[operation start];
}
Last:
- (void)loadAllImages:(NSMutableArray *)imageURLs
{
// ???
}
I'm stuck. What should I do next? I have 20 URLs, but how should I download images and direct them to ViewController to update image in cells?
I suppouse AFNetworkig can provide me some operation queue.
And I dont like my code now. I use this chain, but I want an independent method2 returning imgURLs. So it should look:
User presses button -> method1 -> method2 -> stop. Wait untill user presses button -> download image1 -> show image1 -> download image2 -> show image2 -> and so on -> download imageN -> show imageN -> stop. I'll repeat, I need to store images in Array, I'll use it after that.
Thx u read that.
///////////////////////////////////// UPDATE /////////////////////////////////////
I found solution. But it does not satisfy me completely. Images come randomly. How to make them load in order?
- (void)loadAllImages:(NSMutableArray *)imageURLs
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
for (NSURL *url in imageURLs)
{
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFImageResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
[[UserData sharedUser].images addObject:responseObject];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CollectionViewRealoadData" object:nil];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// show error
}];
[manager.operationQueue addOperation:operation];
}
}
You need to get the data from the URLs and then create a UIImage object from that data.
You can get the data from the URL using the NSURL methods
for(NSString *imgString in imageURLs){
NSURL *url = [NSURL URLWithString:imgString];
NSData *imgData = [NSData dataWithContentsOfURL:url];
UIImage *img = [UIImage imageWithData:imgData ];
[imageUrls addObject:img];//this mutable array should be initialized early in view controller life cycle (like ViewDidLoad).
}
Once you have your image object you can add it to your array of images that you are using as a datasource for your collection view.
[_collectionView insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageUrls count] - 1 inSection:0]]];
//reload your collection view once you add new data
I'm trying to figure out a way to download multiple images with AFNewtorking 2.0. I've read a lot of posts here in SO, but can't find the answer I'm looking for, hope you guys can help me.
The problem is that I want to know when all of the downloads finished and if all images where downloaded.
So I have an array with image URL's ant trying to do something like this.
for(NSString *photoUrlString in self.photos){
NSURL *url = [NSURL URLWithString:photoUrlString];
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:[NSURLRequest requestWithURL:url]];
requestOperation.responseSerializer = [AFImageResponseSerializer serializer];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Image error: %#", error);
}];
[requestOperation start];
}
I've found some answers with putting these requests into a queue and setting max concurrent operations to 1. But don't know how that works really.
Any help is appreciated, thanks in advance!
for(Photo *photo in array){
//form the path where you want to save your downloaded image to
NSString *constPath = [photo imageFullPath];
//url of your photo
NSURL *url = [NSURL URLWithString:photo.serverPath];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:[NSURLRequest requestWithURL:url]];
op.responseSerializer = [AFImageResponseSerializer serializer];
op.outputStream = [NSOutputStream outputStreamToFileAtPath:constPath append:NO];
op.queuePriority = NSOperationQueuePriorityLow;
[op setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead){
}];
op.completionBlock = ^{
//do whatever you want with the downloaded photo, it is stored in the path you create in constPath
};
[requestArray addObject:op];
}
NSArray *batches = [AFURLConnectionOperation batchOfRequestOperations:requestArray progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
} completionBlock:^(NSArray *operations) {
//after all operations are completed this block is called
if (successBlock)
successBlock();
}];
[[NSOperationQueue mainQueue] addOperations:batches waitUntilFinished:NO];
Try this:
// _group, _queue are iVar variable
dispatch_group_t *_group = dispatch_group_create();
dispatch_queue_t *_queue = dispatch_queue_create("com.company.myqueue2", NULL);
// all files download
for(int i = 0 ; i < numberOfFileDownloads; i++){
dispatch_group_async(_group, _queue, ^{
// here is background thread;
// download file
});
}
// all files are download successfully, this method is called
dispatch_group_notify(_group, _queue, ^{
}
Check out +[AFURLConnectionOperation batchOfRequestOperations:progressBlock:completionBlock:]
Although it's not documented, implementation is self-explanatory. Also it allows you to monitor the progress.
You will need to have an array of HTTP operations prior to using this method (this is if you decided to stick to NSURLConnection-based implementation of AFNetworking).